diff --git a/modules/jsonapi_menu_items_tree/README.md b/modules/jsonapi_menu_items_tree/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..34d35db05382ff1a126d8ad6e44dc81c8a187f2a
--- /dev/null
+++ b/modules/jsonapi_menu_items_tree/README.md
@@ -0,0 +1,155 @@
+# Urban Insight JSON API Menu Tree
+
+> Adds a JSON API resource to expose menu items in their original tree structure: `/jsonapi/menu_items_tree/{menu}`
+
+Other JSON:API resources that also expose menu items do so by flattening all data and listing all menu items in a single-level list. Then they put the reference to the parent element in a `parent` attribute. This is how it is done by resources like:
+
+- **The default JSON:API endpoint for the Menu Link Content entities:** `/jsonapi/menu_link_content/menu_link_content?filter[menu_name]=main`
+
+- **[The contrib module JSON:API Menu Items](https://www.drupal.org/project/jsonapi_menu_items)**, which simplifies the obtention of menu items, providing a custom endpoint and the possibility of filtering by link depth, parents, etc: `/jsonapi/menu_items/main`
+
+Let's say you have 4 links A, B, C and D; where A is a first-level element and the parent of B; B in turn is the parent of C, and D is a first-level element, sibling of A and without children.
+
+*  A
+    *  B
+        * C
+*  D
+
+Using any of the default JSON:API endpoint for the Menu Link Content entities:** `/jsonapi/menu_link_content/menu_link_content?filter[menu_name]=main` or the resource provided by the parent module (`/jsonapi/menu_items/main`), the result in the response will be a flattened representation of the menu tree similar to this:
+
+```
+"data": [
+    {
+        "type": "menu_link_content--menu_link_content",
+        "id": "menu_link_content:A",
+        "attributes": {
+            "menu_name": "main",
+            "title": "A",
+            "url": "/node/1",
+            "parent": ""
+        }
+    },
+    {
+        "type": "menu_link_content--menu_link_content",
+        "id": "menu_link_content:B",
+        "attributes": {
+            "menu_name": "main",
+            "title": "B",
+            "url": "/node/2",
+            "parent": "menu_link_content:A"
+        }
+    },
+    {
+        "type": "menu_link_content--menu_link_content",
+        "id": "menu_link_content:C",
+        "attributes": {
+            "menu_name": "main",
+            "title": "C",
+            "url": "/node/3",
+            "parent": "menu_link_content:B"
+        }
+    },
+    {
+        "type": "menu_link_content--menu_link_content",
+        "id": "menu_link_content:D",
+        "attributes": {
+            "menu_name": "main",
+            "title": "D",
+            "url": "/node/4",
+            "parent": ""
+        }
+    }
+]
+```
+By using the JSON:API resource created by this module, you'll get a tree structure instead of a flattened array, which means that external applications don't have to deal with hierarchy, which can make menu building easier in the client app:
+
+```
+"data": [
+    {
+        "type": "menu_link_content--menu_link_content",
+        "id": "menu_link_content:A",
+        "attributes": {
+            "menu_name": "main",
+            "title": "A",
+            "url": "/node/1",
+            "parent": "",
+            "subTree": [
+                {
+                    "type": "menu_link_content--menu_link_content",
+                    "id": "menu_link_content:B",
+                    "attributes": {
+                        "menu_name": "main",
+                        "title": "B",
+                        "url": "/node/2",
+                        "parent": "menu_link_content:A",
+                        "children": [
+                            {
+                              "type": "menu_link_content--menu_link_content",
+                              "id": "menu_link_content:C",
+                              "attributes": {
+                                  "menu_name": "main",
+                                  "title": "C",
+                                  "url": "/node/3",
+                                  "parent": "menu_link_content:B"
+                              }
+                          },
+                        ]
+                    }
+                },
+            ]
+        }
+    },
+    {
+        "type": "menu_link_content--menu_link_content",
+        "id": "menu_link_content:D",
+        "attributes": {
+            "menu_name": "main",
+            "title": "D",
+            "url": "/node/4",
+            "parent": ""
+        }
+    }
+]
+```
+
+## Features
+
+- Supports user and system created menu items.
+- Supports `menu_link_content` and [menu_link_config](https://www.drupal.org/project/menu_link_config) menu items.
+- Supports filtering by depth, parents and custom query conditions.
+
+## Pending
+
+- Add support for fields added to menu links via [Menu Item Extras](https://www.drupal.org/project/menu_item_extras) (includes).
+
+## Filters
+
+- **min_depth**
+
+  Sets the minimum depth of menu links in the resulting tree relative to the root.
+
+  Example: `?filter[min_depth]=2`
+
+- **max_depth**
+
+  Sets the maximum depth of menu links in the resulting tree relative to the root.
+
+  Example: `?filter[max_depth]=2`
+
+- **parent**
+
+  Sets a root for menu tree loading.
+
+  Example: `?filter[parent]=system.admin`
+
+- **parents**
+
+  Adds parent menu links IDs to restrict the tree.
+
+  Example: `?filter[parents]=system.admin,system.admin_structure`
+
+- **conditions[]**
+
+  Adds a custom query condition.
+
+  Example: `?filter[conditions][provider][value]=system`
diff --git a/modules/jsonapi_menu_items_tree/jsonapi_menu_items_tree.info.yml b/modules/jsonapi_menu_items_tree/jsonapi_menu_items_tree.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c2ff79bef11231e1dadf9cd2e51b573b30086cc5
--- /dev/null
+++ b/modules/jsonapi_menu_items_tree/jsonapi_menu_items_tree.info.yml
@@ -0,0 +1,7 @@
+name: JSON:API Menu Tree
+type: module
+description: Provides a JSON API resource to expose a menu tree
+package: Custom
+core_version_requirement: ^9 || ^10
+dependencies:
+  - jsonapi_menu_items:jsonapi_menu_items
\ No newline at end of file
diff --git a/modules/jsonapi_menu_items_tree/jsonapi_menu_items_tree.routing.yml b/modules/jsonapi_menu_items_tree/jsonapi_menu_items_tree.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..30dfe392de3049dad68f0642b29ca070e191c5cf
--- /dev/null
+++ b/modules/jsonapi_menu_items_tree/jsonapi_menu_items_tree.routing.yml
@@ -0,0 +1,10 @@
+jsonapi_menu_items_tree.menu_items_tree:
+  path: '/%jsonapi%/menu_items_tree/{menu}'
+  defaults:
+    _jsonapi_resource: '\Drupal\jsonapi_menu_items_tree\Resource\MenuTreeResource'
+  options:
+    parameters:
+      menu:
+        type: entity:menu
+  requirements:
+    _access: 'TRUE'
diff --git a/modules/jsonapi_menu_items_tree/src/Resource/MenuTreeResource.php b/modules/jsonapi_menu_items_tree/src/Resource/MenuTreeResource.php
new file mode 100644
index 0000000000000000000000000000000000000000..455df46ce736dc99d86e13ab92f84942d0486859
--- /dev/null
+++ b/modules/jsonapi_menu_items_tree/src/Resource/MenuTreeResource.php
@@ -0,0 +1,150 @@
+<?php
+
+namespace Drupal\jsonapi_menu_items_tree\Resource;
+
+use Drupal\Core\Access\AccessResultInterface;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\GeneratedUrl;
+use Drupal\jsonapi\JsonApiResource\LinkCollection;
+use Drupal\jsonapi\JsonApiResource\ResourceObject;
+use Drupal\system\MenuInterface;
+use Drupal\jsonapi_menu_items\Resource\MenuItemsResource;
+
+/**
+ * Processes a request for a menu and delivers a processed menu tree structure.
+ *
+ * @internal
+ */
+class MenuTreeResource extends MenuItemsResource {
+
+  /**
+   * Overrides the parent method to provide a custom menu tree structure.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
+   *   The menu tree.
+   * @param array $items
+   *   The already created items.
+   * @param \Drupal\Core\Cache\CacheableMetadata $cache
+   *   The cacheable metadata.
+   * @param \Drupal\system\MenuInterface $menu
+   *   The menu that the links belong to.
+   */
+  protected function getMenuItems(array $tree, array &$items, CacheableMetadata $cache, MenuInterface $menu) {
+    $items = [];
+    foreach ($tree as $menu_link) {
+
+      if ($menu_link->access !== NULL && !$menu_link->access instanceof AccessResultInterface) {
+        throw new \DomainException('MenuLinkTreeElement::access must be either NULL or an AccessResultInterface object.');
+      }
+
+      if ($menu_link->access instanceof AccessResultInterface) {
+        $cache->merge(CacheableMetadata::createFromObject($menu_link->access));
+      }
+
+      // Only return accessible links.
+      if ($menu_link->access instanceof AccessResultInterface && !$menu_link->access->isAllowed()) {
+        continue;
+      }
+
+      $id = $menu_link->link->getPluginId();
+      [$plugin] = explode(':', $id, 2);
+      if ($plugin === 'menu_link_config') {
+        $resource_type = $this->resourceTypeRepository->get('menu_link_config', 'menu_link_config');
+      }
+      else {
+        $resource_type = $this->resourceTypeRepository->get('menu_link_content', $menu_link->link->getMenuName());
+        if ($resource_type === NULL) {
+          $resource_type = $this->resourceTypeRepository->get('menu_link_content', 'menu_link_content');
+        }
+      }
+
+      $url = $menu_link->link->getUrlObject()->toString(TRUE);
+      assert($url instanceof GeneratedUrl);
+      $cache->addCacheableDependency($url);
+
+      $links = new LinkCollection([]);
+
+      $resource_object_cacheability = new CacheableMetadata();
+      $resource_object_cacheability->addCacheableDependency($menu_link->access);
+      $resource_object_cacheability->addCacheableDependency($cache);
+
+      $fields = $this->buildMenuLinkSubtree($menu_link, $resource_object_cacheability);
+      $items[$id] = new ResourceObject($resource_object_cacheability, $resource_type, $id, NULL, $fields, $links);
+    }
+  }
+
+  /**
+   * Recursively generates the data structure for a menu link and its children.
+   *
+   * Each child link is nested under the `subTree` key of its parent link,
+   * creating a tree-like representation of the menu.
+   *
+   * @param array $menuLink
+   *   The menu link data to process.
+   * @param \Drupal\Core\Cache\CacheableMetadata $cache
+   *   The cacheable metadata object to merge with the menu link's cache.
+   * @param int $depth
+   *   The current depth in the menu tree.
+   *
+   * @return array
+   *   A structured array representing the menu link and its nested children.
+   */
+  public function buildMenuLinkSubtree($menu_link, CacheableMetadata $cache, int $depth = 0) {
+
+    if ($menu_link->access !== NULL && !$menu_link->access instanceof AccessResultInterface) {
+      throw new \DomainException('MenuLinkTreeElement::access must be either NULL or an AccessResultInterface object.');
+    }
+
+    if ($menu_link->access instanceof AccessResultInterface) {
+      $cache->merge(CacheableMetadata::createFromObject($menu_link->access));
+    }
+
+    // Only build data for accessible links.
+    if ($menu_link->access instanceof AccessResultInterface && !$menu_link->access->isAllowed()) {
+      return [];
+    }
+
+    $url = $menu_link->link->getUrlObject()->toString(TRUE);
+    assert($url instanceof GeneratedUrl);
+    $cache->addCacheableDependency($url);
+    $cache->addCacheableDependency($menu_link->access);
+
+    $fields = [
+      'description' => $menu_link->link->getDescription(),
+      'enabled' => $menu_link->link->isEnabled(),
+      'expanded' => $menu_link->link->isExpanded(),
+      'menu_name' => $menu_link->link->getMenuName(),
+      'meta' => $menu_link->link->getMetaData(),
+      'options' => $menu_link->link->getOptions(),
+      'parent' => $menu_link->link->getParent(),
+      'provider' => $menu_link->link->getProvider(),
+      'route' => [
+        'name' => $menu_link->link->getRouteName(),
+        'parameters' => $menu_link->link->getRouteParameters(),
+      ],
+      'title' => (string) $menu_link->link->getTitle(),
+      'url' => $url->getGeneratedUrl(),
+      'weight' => (int) $menu_link->link->getWeight(),
+      'depth' => $depth,
+    ];
+
+    if ($menu_link->subtree) {
+      $depth++;
+      foreach ($menu_link->subtree as $link) {
+        /** @var \Drupal\Core\Menu\MenuLinkTreeElement $link */
+        if ($link->access instanceof AccessResultInterface && !$link->access->isAllowed()) {
+          continue;
+        }
+        $provider = $link->link->getProvider();
+        $child_data = [
+          "type" => $provider . '--' . $provider,
+          "id" => $link->link->getPluginId(),
+          "attributes" => $this->buildMenuLinkSubtree($link, $cache, $depth),
+        ];
+        $fields['subTree'][] = $child_data;
+      }
+    }
+    return $fields;
+  }
+
+}
diff --git a/src/Resource/MenuItemsResource.php b/src/Resource/MenuItemsResource.php
index 045cc8d31f0651036426c3bee73a0b0fbe8d7836..bf0d3223099bc02998c1f4af9e19ada4202c44c2 100644
--- a/src/Resource/MenuItemsResource.php
+++ b/src/Resource/MenuItemsResource.php
@@ -30,7 +30,7 @@ use Symfony\Component\Routing\Route;
  *
  * @internal
  */
-final class MenuItemsResource extends ResourceBase implements ContainerInjectionInterface {
+class MenuItemsResource extends ResourceBase implements ContainerInjectionInterface {
 
   /**
    * A list of menu items.
@@ -44,35 +44,35 @@ final class MenuItemsResource extends ResourceBase implements ContainerInjection
    *
    * @var \Drupal\system\MenuInterface
    */
-  private $menuLinkTree;
+  protected $menuLinkTree;
 
   /**
    * The entity type manager service.
    *
    * @var \Drupal\Core\Entity\EntityTypeManagerInterface
    */
-  private $entityTypeManager;
+  protected $entityTypeManager;
 
   /**
    * The entity field manager service.
    *
    * @var \Drupal\Core\Entity\EntityFieldManagerInterface
    */
-  private $entityFieldManager;
+  protected $entityFieldManager;
 
   /**
    * The cache backend.
    *
    * @var \Drupal\Core\Cache\CacheBackendInterface
    */
-  private $cache;
+  protected $cache;
 
   /**
    * The entity repository.
    *
    * @var \Drupal\Core\Entity\EntityRepositoryInterface
    */
-  private $entityRepository;
+  protected $entityRepository;
 
   /**
    * Construct a new MenuItemsResource object.
@@ -100,7 +100,7 @@ final class MenuItemsResource extends ResourceBase implements ContainerInjection
    * {@inheritDoc}
    */
   public static function create(ContainerInterface $container) {
-    return new self(
+    return new static(
       $container->get('menu.link_tree'),
       $container->get('entity_type.manager'),
       $container->get('entity_field.manager'),