diff --git a/core/modules/jsonapi/src/CacheableResourceResponse.php b/core/modules/jsonapi/src/CacheableResourceResponse.php
new file mode 100644
index 0000000000..4692ae2a06
--- /dev/null
+++ b/core/modules/jsonapi/src/CacheableResourceResponse.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\jsonapi;
+
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\Cache\CacheableResponseTrait;
+
+/**
+ * Extends ResourceResponse with cacheability.
+ *
+ * We want to have the same functionality for both responses that are cacheable
+ * and those that are not.  This response class should be used in all instances
+ * where the response is expected to be cacheable.  This approach is similar
+ * to the REST module's ModifiedResourceResponse.
+ *
+ * @internal JSON:API maintains no PHP API since its API is the HTTP API. This
+ *   class may change at any time and this will break any dependencies on it.
+ *
+ * @see https://www.drupal.org/project/jsonapi/issues/3032787
+ * @see jsonapi.api.php
+ *
+ * @see \Drupal\rest\ModifiedResourceResponse
+ */
+class CacheableResourceResponse extends ResourceResponse implements CacheableResponseInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/modules/jsonapi/src/Controller/EntityResource.php b/core/modules/jsonapi/src/Controller/EntityResource.php
index 8c0e027aa3..749a21207d 100644
--- a/core/modules/jsonapi/src/Controller/EntityResource.php
+++ b/core/modules/jsonapi/src/Controller/EntityResource.php
@@ -26,6 +26,7 @@
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
 use Drupal\jsonapi\Access\EntityAccessChecker;
+use Drupal\jsonapi\CacheableResourceResponse;
 use Drupal\jsonapi\Context\FieldResolver;
 use Drupal\jsonapi\Entity\EntityValidationTrait;
 use Drupal\jsonapi\Access\TemporaryQueryGuard;
@@ -272,7 +273,6 @@ public function createIndividual(ResourceType $resource_type, Request $request)
     // we should send "Location" header to the frontend.
     if ($resource_type->isLocatable()) {
       $url = $resource_object->toUrl()->setAbsolute()->toString(TRUE);
-      $response->addCacheableDependency($url);
       $response->headers->set('Location', $url->getGeneratedUrl());
     }
 
@@ -520,7 +520,9 @@ function (EntityInterface $entity) {
 
     // $response does not contain the entity list cache tag. We add the
     // cacheable metadata for the finite list of entities in the relationship.
-    $response->addCacheableDependency($entity);
+    if ($response instanceof CacheableResourceResponse) {
+      $response->addCacheableDependency($entity);
+    }
 
     return $response;
   }
@@ -553,7 +555,9 @@ public function getRelationship(ResourceType $resource_type, FieldableEntityInte
       return $links->withLink($key, new Link(new CacheableMetadata(), $relationship_object_urls[$key], [$key]));
     }, new LinkCollection([])));
     // Add the host entity as a cacheable dependency.
-    $response->addCacheableDependency($entity);
+    if ($response instanceof CacheableResourceResponse) {
+      $response->addCacheableDependency($entity);
+    }
     return $response;
   }
 
@@ -980,14 +984,20 @@ protected function buildWrappedResponse($data, Request $request, IncludedData $i
       $self_link = new Link(new CacheableMetadata(), self::getRequestLink($request), ['self']);
       $links = $links->withLink('self', $self_link);
     }
-    $response = new ResourceResponse(new JsonApiDocumentTopLevel($data, $includes, $links, $meta), $response_code, $headers);
-    $cacheability = (new CacheableMetadata())->addCacheContexts([
-      // Make sure that different sparse fieldsets are cached differently.
-      'url.query_args:fields',
-      // Make sure that different sets of includes are cached differently.
-      'url.query_args:include',
-    ]);
-    $response->addCacheableDependency($cacheability);
+    $document = new JsonApiDocumentTopLevel($data, $includes, $links, $meta);
+    if ($request->isMethodCacheable()) {
+      $response = new CacheableResourceResponse($document, $response_code, $headers);
+      $cacheability = (new CacheableMetadata())->addCacheContexts([
+        // Make sure that different sparse fieldsets are cached differently.
+        'url.query_args:fields',
+        // Make sure that different sets of includes are cached differently.
+        'url.query_args:include',
+      ]);
+      $response->addCacheableDependency($cacheability);
+    }
+    else {
+      $response = new ResourceResponse($document, $response_code, $headers);
+    }
     return $response;
   }
 
diff --git a/core/modules/jsonapi/src/Controller/EntryPoint.php b/core/modules/jsonapi/src/Controller/EntryPoint.php
index bbbc2d56d1..d00f52be86 100644
--- a/core/modules/jsonapi/src/Controller/EntryPoint.php
+++ b/core/modules/jsonapi/src/Controller/EntryPoint.php
@@ -6,12 +6,12 @@
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
+use Drupal\jsonapi\CacheableResourceResponse;
 use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
 use Drupal\jsonapi\JsonApiResource\LinkCollection;
 use Drupal\jsonapi\JsonApiResource\NullIncludedData;
 use Drupal\jsonapi\JsonApiResource\Link;
 use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
-use Drupal\jsonapi\ResourceResponse;
 use Drupal\jsonapi\ResourceType\ResourceType;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
 use Drupal\user\Entity\User;
@@ -116,7 +116,7 @@ public function index() {
       }
     }
 
-    $response = new ResourceResponse(new JsonApiDocumentTopLevel(new ResourceObjectData([]), new NullIncludedData(), $urls, $meta));
+    $response = new CacheableResourceResponse(new JsonApiDocumentTopLevel(new ResourceObjectData([]), new NullIncludedData(), $urls, $meta));
     return $response->addCacheableDependency($cacheability);
   }
 
diff --git a/core/modules/jsonapi/src/EventSubscriber/DefaultExceptionSubscriber.php b/core/modules/jsonapi/src/EventSubscriber/DefaultExceptionSubscriber.php
index 0220ef55dd..b761d54931 100644
--- a/core/modules/jsonapi/src/EventSubscriber/DefaultExceptionSubscriber.php
+++ b/core/modules/jsonapi/src/EventSubscriber/DefaultExceptionSubscriber.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\jsonapi\EventSubscriber;
 
+use Drupal\jsonapi\CacheableResourceResponse;
 use Drupal\jsonapi\JsonApiResource\ErrorCollection;
 use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
 use Drupal\jsonapi\JsonApiResource\LinkCollection;
@@ -58,8 +59,14 @@ public function onException(GetResponseForExceptionEvent $event) {
   protected function setEventResponse(GetResponseForExceptionEvent $event, $status) {
     /* @var \Symfony\Component\HttpKernel\Exception\HttpException $exception */
     $exception = $event->getException();
-    $response = new ResourceResponse(new JsonApiDocumentTopLevel(new ErrorCollection([$exception]), new NullIncludedData(), new LinkCollection([])), $exception->getStatusCode(), $exception->getHeaders());
-    $response->addCacheableDependency($exception);
+    $document = new JsonApiDocumentTopLevel(new ErrorCollection([$exception]), new NullIncludedData(), new LinkCollection([]));
+    if ($event->getRequest()->isMethodCacheable()) {
+      $response = new CacheableResourceResponse($document, $exception->getStatusCode(), $exception->getHeaders());
+      $response->addCacheableDependency($exception);
+    }
+    else {
+      $response = new ResourceResponse($document, $exception->getStatusCode(), $exception->getHeaders());
+    }
     $event->setResponse($response);
   }
 
diff --git a/core/modules/jsonapi/src/EventSubscriber/ResourceResponseSubscriber.php b/core/modules/jsonapi/src/EventSubscriber/ResourceResponseSubscriber.php
index 9bf6aaa43a..8fff7e4406 100644
--- a/core/modules/jsonapi/src/EventSubscriber/ResourceResponseSubscriber.php
+++ b/core/modules/jsonapi/src/EventSubscriber/ResourceResponseSubscriber.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Cache\CacheableResponse;
 use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\jsonapi\CacheableResourceResponse;
 use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
 use Drupal\jsonapi\ResourceResponse;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -121,9 +122,11 @@ protected function renderResponseBody(Request $request, ResourceResponse $respon
       // Having just normalized the data, we can associate its cacheability with
       // the response object.
       assert($jsonapi_doc_object instanceof CacheableNormalization);
-      $response->addCacheableDependency($jsonapi_doc_object);
       // Finally, encode the normalized data (JSON:API's encoder rasterizes it
       // automatically).
+      if ($response instanceof CacheableResourceResponse) {
+        $response->addCacheableDependency($jsonapi_doc_object);
+      }
       $response->setContent($serializer->encode($jsonapi_doc_object->getNormalization(), $format));
       $response->headers->set('Content-Type', $request->getMimeType($format));
     }
diff --git a/core/modules/jsonapi/src/ResourceResponse.php b/core/modules/jsonapi/src/ResourceResponse.php
index b73165d170..5bfbe930e7 100644
--- a/core/modules/jsonapi/src/ResourceResponse.php
+++ b/core/modules/jsonapi/src/ResourceResponse.php
@@ -2,8 +2,6 @@
 
 namespace Drupal\jsonapi;
 
-use Drupal\Core\Cache\CacheableResponseInterface;
-use Drupal\Core\Cache\CacheableResponseTrait;
 use Symfony\Component\HttpFoundation\Response;
 
 /**
@@ -22,9 +20,7 @@
  *
  * @see \Drupal\rest\ModifiedResourceResponse
  */
-class ResourceResponse extends Response implements CacheableResponseInterface {
-
-  use CacheableResponseTrait;
+class ResourceResponse extends Response {
 
   /**
    * Response data that should be serialized.
diff --git a/core/modules/jsonapi/tests/src/Functional/ResourceResponseTestTrait.php b/core/modules/jsonapi/tests/src/Functional/ResourceResponseTestTrait.php
index 04a173ff71..342e55ff6b 100644
--- a/core/modules/jsonapi/tests/src/Functional/ResourceResponseTestTrait.php
+++ b/core/modules/jsonapi/tests/src/Functional/ResourceResponseTestTrait.php
@@ -11,8 +11,8 @@
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\RevisionableInterface;
 use Drupal\Core\Url;
+use Drupal\jsonapi\CacheableResourceResponse;
 use Drupal\jsonapi\Normalizer\HttpExceptionNormalizer;
-use Drupal\jsonapi\ResourceResponse;
 use Psr\Http\Message\ResponseInterface;
 
 /**
@@ -38,7 +38,7 @@
    *   be deduced from the number of responses, because a multiple cardinality
    *   field may have only one value.
    *
-   * @return \Drupal\jsonapi\ResourceResponse
+   * @return \Drupal\jsonapi\CacheableResourceResponse
    *   The merged ResourceResponse.
    */
   protected static function toCollectionResourceResponse(array $responses, $self_link, $is_multiple) {
@@ -103,7 +103,7 @@ protected static function toCollectionResourceResponse(array $responses, $self_l
     // individual resources in those collections, which means any '4xx-response'
     // cache tags on the individual responses should also be omitted.
     $merged_cacheability->setCacheTags(array_diff($merged_cacheability->getCacheTags(), ['4xx-response']));
-    return (new ResourceResponse($merged_document, 200))->addCacheableDependency($merged_cacheability);
+    return (new CacheableResourceResponse($merged_document, 200))->addCacheableDependency($merged_cacheability);
   }
 
   /**
@@ -203,7 +203,7 @@ protected function getExpectedIncludedResourceResponse(array $include_paths, arr
     $basic_cacheability = (new CacheableMetadata())
       ->addCacheTags($this->getExpectedCacheTags())
       ->addCacheContexts($this->getExpectedCacheContexts());
-    return static::decorateExpectedResponseForIncludedFields(ResourceResponse::create($individual_document), $resource_data['responses'])
+    return static::decorateExpectedResponseForIncludedFields(CacheableResourceResponse::create($individual_document), $resource_data['responses'])
       ->addCacheableDependency($basic_cacheability);
   }
 
@@ -230,7 +230,7 @@ protected static function toResourceResponses(array $responses) {
    * @param \Psr\Http\Message\ResponseInterface $response
    *   A PSR response to be mapped.
    *
-   * @return \Drupal\jsonapi\ResourceResponse
+   * @return \Drupal\jsonapi\CacheableResourceResponse
    *   The ResourceResponse.
    */
   protected static function toResourceResponse(ResponseInterface $response) {
@@ -245,7 +245,7 @@ protected static function toResourceResponse(ResponseInterface $response) {
       $cacheability->setCacheMaxAge(($dynamic_cache[0] === 'UNCACHEABLE' && $response->getStatusCode() < 400) ? 0 : Cache::PERMANENT);
     }
     $related_document = Json::decode($response->getBody());
-    $resource_response = new ResourceResponse($related_document, $response->getStatusCode());
+    $resource_response = new CacheableResourceResponse($related_document, $response->getStatusCode());
     return $resource_response->addCacheableDependency($cacheability);
   }
 
@@ -496,7 +496,7 @@ protected function getResponses(array $links, array $request_options) {
    *   (optional) Document pointer for the JSON:API error object. FALSE to omit
    *   the pointer.
    *
-   * @return \Drupal\jsonapi\ResourceResponse
+   * @return \Drupal\jsonapi\CacheableResourceResponse
    *   The forbidden ResourceResponse.
    */
   protected static function getAccessDeniedResponse(EntityInterface $entity, AccessResultInterface $access, Url $via_link, $relationship_field_name = NULL, $detail = NULL, $pointer = NULL) {
@@ -519,7 +519,7 @@ protected static function getAccessDeniedResponse(EntityInterface $entity, Acces
       $error['links']['via']['href'] = $via_link->setAbsolute()->toString();
     }
 
-    return (new ResourceResponse([
+    return (new CacheableResourceResponse([
       'jsonapi' => static::$jsonApiMember,
       'errors' => [$error],
     ], 403))
@@ -536,7 +536,7 @@ protected static function getAccessDeniedResponse(EntityInterface $entity, Acces
    * @param string $self_link
    *   The self link for collection ResourceResponse.
    *
-   * @return \Drupal\jsonapi\ResourceResponse
+   * @return \Drupal\jsonapi\CacheableResourceResponse
    *   The empty collection ResourceResponse.
    */
   protected function getEmptyCollectionResponse($cardinality, $self_link) {
@@ -549,7 +549,7 @@ protected function getEmptyCollectionResponse($cardinality, $self_link) {
       'url.site',
     ], $this->entity->getEntityType()->isRevisionable() ? ['url.query_args:resourceVersion'] : []);
     $cacheability = (new CacheableMetadata())->addCacheContexts($cache_contexts)->addCacheTags(['http_response']);
-    return (new ResourceResponse([
+    return (new CacheableResourceResponse([
       // Empty to-one relationships should be NULL and empty to-many
       // relationships should be an empty array.
       'data' => $cardinality === 1 ? NULL : [],
diff --git a/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php b/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php
index 6257adb4d8..a963c055b2 100644
--- a/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php
+++ b/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php
@@ -27,6 +27,7 @@
 use Drupal\Core\Url;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\jsonapi\CacheableResourceResponse;
 use Drupal\jsonapi\JsonApiResource\LinkCollection;
 use Drupal\jsonapi\JsonApiResource\NullIncludedData;
 use Drupal\jsonapi\JsonApiResource\Link;
@@ -1256,7 +1257,7 @@ protected function getExpectedCollectionResponse(array $collection, $self_link,
     $cacheability = static::getExpectedCollectionCacheability($this->account, $collection, NULL, $filtered);
     $cacheability->setCacheMaxAge($merged_response->getCacheableMetadata()->getCacheMaxAge());
 
-    $collection_response = ResourceResponse::create($merged_document);
+    $collection_response = CacheableResourceResponse::create($merged_document);
     $collection_response->addCacheableDependency($cacheability);
 
     if (is_null($included_paths)) {
@@ -1671,7 +1672,7 @@ protected function doTestRelationshipMutation(array $request_options) {
    * @param \Drupal\Core\Entity\EntityInterface|null $entity
    *   (optional) The entity for which to get expected relationship response.
    *
-   * @return \Drupal\jsonapi\ResourceResponse
+   * @return \Drupal\jsonapi\CacheableResourceResponse
    *   The expected ResourceResponse.
    */
   protected function getExpectedGetRelationshipResponse($relationship_field_name, EntityInterface $entity = NULL) {
@@ -1696,7 +1697,7 @@ protected function getExpectedGetRelationshipResponse($relationship_field_name,
       ->addCacheableDependency($entity)
       ->addCacheableDependency($access);
     $status_code = isset($expected_document['errors'][0]['status']) ? $expected_document['errors'][0]['status'] : 200;
-    $resource_response = new ResourceResponse($expected_document, $status_code);
+    $resource_response = new CacheableResourceResponse($expected_document, $status_code);
     $resource_response->addCacheableDependency($expected_cacheability);
     return $resource_response;
   }
@@ -1844,7 +1845,7 @@ protected function getExpectedRelatedResponse($relationship_field_name, array $r
         $cacheability = (new CacheableMetadata())->addCacheContexts($cache_contexts)->addCacheTags(['http_response']);
         $related_response = isset($relationship_document['errors'])
           ? $relationship_response
-          : (new ResourceResponse(static::getEmptyCollectionResponse(!is_null($relationship_document['data']), $self_link)->getResponseData()))->addCacheableDependency($cacheability);
+          : (new CacheableResourceResponse(static::getEmptyCollectionResponse(!is_null($relationship_document['data']), $self_link)->getResponseData()))->addCacheableDependency($cacheability);
       }
       else {
         $is_to_one_relationship = static::isResourceIdentifier($relationship_document['data']);
@@ -3159,15 +3160,15 @@ public function testRevisions() {
    * the expected cacheability for those includes. It does so based of responses
    * from the related routes for individual relationships.
    *
-   * @param \Drupal\jsonapi\ResourceResponse $expected_response
+   * @param \Drupal\jsonapi\CacheableResourceResponse $expected_response
    *   The expected ResourceResponse.
    * @param \Drupal\jsonapi\ResourceResponse[] $related_responses
    *   The related ResourceResponses, keyed by relationship field names.
    *
-   * @return \Drupal\jsonapi\ResourceResponse
+   * @return \Drupal\jsonapi\CacheableResourceResponse
    *   The decorated ResourceResponse.
    */
-  protected static function decorateExpectedResponseForIncludedFields(ResourceResponse $expected_response, array $related_responses) {
+  protected static function decorateExpectedResponseForIncludedFields(CacheableResourceResponse $expected_response, array $related_responses) {
     $expected_document = $expected_response->getResponseData();
     $expected_cacheability = $expected_response->getCacheableMetadata();
     foreach ($related_responses as $related_response) {
@@ -3194,17 +3195,17 @@ protected static function decorateExpectedResponseForIncludedFields(ResourceResp
         }
       }
     }
-    return (new ResourceResponse($expected_document))->addCacheableDependency($expected_cacheability);
+    return (new CacheableResourceResponse($expected_document))->addCacheableDependency($expected_cacheability);
   }
 
   /**
    * Gets the expected individual ResourceResponse for GET.
    *
-   * @return \Drupal\jsonapi\ResourceResponse
+   * @return \Drupal\jsonapi\CacheableResourceResponse
    *   The expected individual ResourceResponse.
    */
   protected function getExpectedGetIndividualResourceResponse($status_code = 200) {
-    $resource_response = new ResourceResponse($this->getExpectedDocument(), $status_code);
+    $resource_response = new CacheableResourceResponse($this->getExpectedDocument(), $status_code);
     $cacheability = new CacheableMetadata();
     $cacheability->setCacheContexts($this->getExpectedCacheContexts());
     $cacheability->setCacheTags($this->getExpectedCacheTags());
diff --git a/core/modules/jsonapi/tests/src/Kernel/Controller/EntityResourceTest.php b/core/modules/jsonapi/tests/src/Kernel/Controller/EntityResourceTest.php
index d60d66dcf4..643fa40b46 100644
--- a/core/modules/jsonapi/tests/src/Kernel/Controller/EntityResourceTest.php
+++ b/core/modules/jsonapi/tests/src/Kernel/Controller/EntityResourceTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\jsonapi\Kernel\Controller;
 
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\jsonapi\CacheableResourceResponse;
 use Drupal\jsonapi\ResourceType\ResourceType;
 use Drupal\jsonapi\JsonApiResource\Data;
 use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
@@ -200,6 +201,7 @@ public function testGetPagedCollection() {
     $response = $entity_resource->getCollection($resource_type, $request);
 
     // Assertions.
+    $this->assertInstanceof(CacheableResourceResponse::class, $response);
     $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
     $this->assertInstanceOf(Data::class, $response->getResponseData()->getData());
     $data = $response->getResponseData()->getData();
@@ -220,6 +222,7 @@ public function testGetEmptyCollection() {
     $response = $this->entityResource->getCollection($resource_type, $request);
 
     // Assertions.
+    $this->assertInstanceof(CacheableResourceResponse::class, $response);
     $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
     $this->assertInstanceOf(Data::class, $response->getResponseData()->getData());
     $this->assertEquals(0, $response->getResponseData()->getData()->count());
