diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c914e6fb06cbedd2f0b9069465c17e3cb89cff92..e41b5000dd58bdda98e8636ddc636dd123cace63 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -48,6 +48,7 @@ include:
 ################
 variables:
   OPT_IN_TEST_PREVIOUS_MAJOR: 1
+  CORE_PREVIOUS_PHP_MIN: 8.0
   OPT_IN_TEST_PREVIOUS_MINOR: 1
   OPT_IN_TEST_NEXT_MINOR: 1
 
diff --git a/src/Unstable/ResourceResponseFactory.php b/src/Unstable/ResourceResponseFactory.php
index a5429fbfec62eb9d890889db53ca235b7f1c623c..0637b4877bff61b50c698b91ff7867570a333996 100644
--- a/src/Unstable/ResourceResponseFactory.php
+++ b/src/Unstable/ResourceResponseFactory.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
 namespace Drupal\jsonapi_resources\Unstable;
 
 use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Http\Exception\CacheableBadRequestHttpException;
 use Drupal\Core\Url;
 use Drupal\jsonapi\CacheableResourceResponse;
 use Drupal\jsonapi\IncludeResolver;
@@ -13,6 +14,7 @@ use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
 use Drupal\jsonapi\JsonApiResource\Link;
 use Drupal\jsonapi\JsonApiResource\LinkCollection;
 use Drupal\jsonapi\JsonApiResource\NullIncludedData;
+use Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface;
 use Drupal\jsonapi\JsonApiResource\ResourceObject;
 use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
 use Drupal\jsonapi\ResourceResponse;
@@ -97,21 +99,83 @@ final class ResourceResponseFactory {
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
    *   The request object.
-   * @param \Drupal\jsonapi\JsonApiResource\ResourceObject|\Drupal\jsonapi\JsonApiResource\ResourceObjectData $data
+   * @param \Drupal\jsonapi\JsonApiResource\ResourceObjectData|\Drupal\jsonapi\JsonApiResource\ResourceObject $data
    *   The response data from which to resolve includes.
    *
    * @return \Drupal\jsonapi\JsonApiResource\IncludedData
-   *   A Data object to be included or a NullData object if the request does not
+   *   A Data object to be included or a NullData object if the request does
+   *   not
    *   specify any include paths.
    *
    * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
    * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
    */
-  private function getIncludes(Request $request, $data): IncludedData {
+  private function getIncludes(Request $request, ResourceObjectData|ResourceObject $data): IncludedData {
     assert($data instanceof ResourceObject || $data instanceof ResourceObjectData);
-    return $request->query->has('include') && ($include_parameter = $request->query->get('include')) && !empty($include_parameter)
-      ? $this->includeResolver->resolve($data, $include_parameter)
-      : new NullIncludedData();
+    if (!$request->query->has('include')) {
+      return new NullIncludedData();
+    }
+    $include_parameter = $request->query->get('include');
+    if (empty($include_parameter)) {
+      return new NullIncludedData();
+    }
+    if ($data instanceof ResourceObject) {
+      return $this->includeResolver->resolve($data, $include_parameter);
+    }
+
+    // Group resource objects to optimize IncludeResolver::toIncludeTree.
+    $data_by_resource_type = [];
+    $relatable_resource_types = [];
+    foreach ($data as $resource_object) {
+      assert($resource_object instanceof ResourceIdentifierInterface);
+      if (!isset($data_by_resource_type[$resource_object->getTypeName()])) {
+        $data_by_resource_type[$resource_object->getTypeName()] = [];
+        $relatable_resource_types[] = array_keys($resource_object->getResourceType()->getRelatableResourceTypes());
+      }
+      $data_by_resource_type[$resource_object->getTypeName()][] = $resource_object;
+    }
+
+    $include_paths = array_map('trim', explode(',', $include_parameter));
+    $unresolved_include_paths = [];
+    $included_data = [];
+    foreach ($data_by_resource_type as $resource_objects) {
+      foreach ($include_paths as $include_path) {
+        try {
+          $included_data[] = $this->includeResolver->resolve(
+            new ResourceObjectData($resource_objects),
+            $include_path
+          );
+          $unresolved_include_paths[$include_path] = FALSE;
+        }
+        catch (\Exception $e) {
+          if (!isset($unresolved_include_paths[$include_path])) {
+            $unresolved_include_paths[$include_path] = TRUE;
+          }
+        }
+      }
+    }
+
+    if (count(array_filter($unresolved_include_paths)) > 0) {
+      // Throw an error if invalid include paths provided.
+      // @see \Drupal\jsonapi\Context\FieldResolver::resolveInternalIncludePath().
+      $cacheability = (new CacheableMetadata())->addCacheContexts(['url.query_args:include']);
+      $message = sprintf(
+        '%s are not valid relationship names.',
+        implode(',', array_map(static fn (string $path) => "`$path`", array_keys($unresolved_include_paths)))
+      );
+      if (count($relatable_resource_types) > 0) {
+        $message .= sprintf(' Possible values: %s', implode(', ', array_unique(array_merge(...$relatable_resource_types))));
+      }
+      throw new CacheableBadRequestHttpException($cacheability, $message);
+    }
+
+    $included_data = array_reduce(
+      $included_data,
+      [IncludedData::class, 'merge'],
+      new IncludedData([])
+    );
+
+    return IncludedData::deduplicate($included_data);
   }
 
 }
diff --git a/tests/src/Kernel/ResourceResponseFactoryTest.php b/tests/src/Kernel/ResourceResponseFactoryTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..5a8a5699fa337048f20a3db02d764c34a184a708
--- /dev/null
+++ b/tests/src/Kernel/ResourceResponseFactoryTest.php
@@ -0,0 +1,219 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\jsonapi_resources\Kernel;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\jsonapi\CacheableResourceResponse;
+use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
+use Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface;
+use Drupal\jsonapi\JsonApiResource\ResourceObject;
+use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\node\NodeInterface;
+use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
+use Drupal\Tests\user\Traits\UserCreationTrait;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Tests ResourceResponseFactory.
+ *
+ * @coversDefaultClass \Drupal\jsonapi_resources\Unstable\ResourceResponseFactory
+ * @group jsonapi_resources
+ */
+final class ResourceResponseFactoryTest extends KernelTestBase {
+
+  use EntityReferenceTestTrait;
+  use UserCreationTrait;
+
+  private const NODE_TYPE_ARTICLE_UUID = 'e5da5021-d7a0-4606-a21c-9586a8cf79a4';
+
+  private const NODE_TYPE_PAGE_UUID = '8378b97d-36fd-4515-b2eb-22e90dfdc8dc';
+
+  private const NODE_TYPE_EVENT_UUID = '12cce39f-fa9c-4c64-b7f6-a0ec511ba1e7';
+
+  private const NODE_ARTICLE_1_UUID = '7bf77016-93d2-4098-84e4-c2634c4d8ecf';
+
+  private const NODE_ARTICLE_2_UUID = '36405873-6b42-44ec-9f47-b771d83149b1';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'system',
+    'user',
+    'field',
+    'file',
+    'serialization',
+    'jsonapi',
+    'jsonapi_resources',
+    'node',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('node');
+
+    $this->account = $this->createUser();
+    $this->container->get('current_user')->setAccount($this->account);
+
+    NodeType::create([
+      'uuid' => self::NODE_TYPE_ARTICLE_UUID,
+      'name' => 'article',
+      'type' => 'article',
+    ])->save();
+    NodeType::create([
+      'uuid' => self::NODE_TYPE_PAGE_UUID,
+      'name' => 'page',
+      'type' => 'page',
+    ])->save();
+    NodeType::create([
+      'uuid' => self::NODE_TYPE_EVENT_UUID,
+      'name' => 'event',
+      'type' => 'event',
+    ])->save();
+    $this->createEntityReferenceField(
+      'node',
+      'page',
+      'field_related_articles',
+      'Related articles',
+      'node',
+      'default',
+      [
+        'target_bundles' => [
+          'reminder' => 'article',
+        ],
+      ],
+      FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
+    );
+
+    $this->container->get('router.builder')->rebuildIfNeeded();
+  }
+
+  /**
+   * @covers ::create
+   * @dataProvider createData
+   */
+  public function testCreate(
+    array $query,
+    array $expected_includes,
+    string $expected_error = '',
+  ): void {
+    if ($expected_error !== '') {
+      $this->expectExceptionMessage($expected_error);
+    }
+
+    $article1 = Node::create([
+      'uuid' => self::NODE_ARTICLE_1_UUID,
+      'type' => 'article',
+      'title' => $this->randomString(),
+      'status' => 1,
+    ]);
+    $article1->save();
+    $article2 = Node::create([
+      'uuid' => self::NODE_ARTICLE_2_UUID,
+      'type' => 'article',
+      'title' => $this->randomString(),
+      'status' => 1,
+    ]);
+    $article2->save();
+    $page = Node::create([
+      'type' => 'page',
+      'title' => $this->randomString(),
+      'status' => 1,
+      'field_related_articles' => [$article1->id(), $article2->id()],
+    ]);
+    $page->save();
+    $event = Node::create([
+      'type' => 'event',
+      'title' => $this->randomString(),
+      'status' => 1,
+    ]);
+    $event->save();
+
+    $resource_type_repository = $this->container->get('jsonapi.resource_type.repository');
+    $resource_objects = array_map(
+      static fn (NodeInterface $node) => ResourceObject::createFromEntity(
+        $resource_type_repository->get($node->getEntityTypeId(), $node->bundle()),
+        $node
+      ),
+      [$article1, $page, $article2, $event]
+    );
+
+    $request = Request::create('/foo?' . http_build_query($query));
+
+    $sut = $this->container->get('jsonapi_resources.resource_response_factory');
+    $response = $sut->create(
+      new ResourceObjectData($resource_objects),
+      $request
+    );
+    self::assertInstanceOf(CacheableResourceResponse::class, $response);
+    $document_top_level = $response->getResponseData();
+    self::assertInstanceOf(JsonApiDocumentTopLevel::class, $document_top_level);
+    /** @var \Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface[] $includes_data */
+    $includes_data = $document_top_level->getIncludes()->toArray();
+    $includes_data = array_map(
+      static fn (ResourceIdentifierInterface $identifier) => [
+        'id' => $identifier->getId(),
+        'type' => $identifier->getTypeName(),
+      ],
+      $includes_data
+    );
+    self::assertEquals($expected_includes, $includes_data);
+  }
+
+  /**
+   * Test data for testCreate.
+   *
+   * @return array[]
+   *   The test data.
+   */
+  public static function createData(): array {
+    return [
+      'mixed resource objects with same include' => [
+        ['include' => 'node_type'],
+        [
+          [
+            'id' => self::NODE_TYPE_ARTICLE_UUID,
+            'type' => 'node_type--node_type',
+          ],
+          [
+            'id' => self::NODE_TYPE_PAGE_UUID,
+            'type' => 'node_type--node_type',
+          ],
+          [
+            'id' => self::NODE_TYPE_EVENT_UUID,
+            'type' => 'node_type--node_type',
+          ],
+        ],
+      ],
+      'mixed resource objects with mismatched includes' => [
+        ['include' => 'field_related_articles'],
+        [
+          [
+            'id' => self::NODE_ARTICLE_1_UUID,
+            'type' => 'node--article',
+          ],
+          [
+            'id' => self::NODE_ARTICLE_2_UUID,
+            'type' => 'node--article',
+          ],
+        ],
+      ],
+      'missing relationship in includes' => [
+        ['include' => 'field_foobar'],
+        [],
+        'field_foobar` are not valid relationship names. Possible values: node_type, revision_uid, uid, field_related_articles',
+      ],
+    ];
+  }
+
+}