Nested Relations

Nuxt Auto API supports advanced nested relation loading with field selection, filtering, and pagination at each level of nesting.

Nested Relations

Nuxt Auto API supports advanced nested relation loading with field selection, filtering, and pagination at each level of nesting.

Basic Nested Relations

The simplest form uses dot notation to load nested relations:

// GET /api/users?include=posts.comments.author

{
  "data": [
    {
      "id": 1,
      "name": "John Doe",
      "posts": [
        {
          "id": 1,
          "title": "Hello World",
          "comments": [
            {
              "id": 1,
              "body": "Great post!",
              "author": {
                "id": 2,
                "name": "Jane Smith"
              }
            }
          ]
        }
      ]
    }
  ]
}

Field Selection

Select specific fields from relations using bracket syntax:

// GET /api/users?include=posts[id,title,createdAt]

{
  "data": [
    {
      "id": 1,
      "name": "John Doe",
      "posts": [
        {
          "id": 1,
          "title": "Hello World",
          "createdAt": "2024-01-01T00:00:00Z"
          // Only selected fields included
        }
      ]
    }
  ]
}

Filtering Relations

Filter related records using curly brace syntax:

// GET /api/users?include=posts{filter:{published:true}}

// Only published posts will be included
{
  "data": [
    {
      "id": 1,
      "name": "John Doe",
      "posts": [
        // Only posts where published = true
      ]
    }
  ]
}

Advanced Filtering

Use the same filter operators as the main list endpoint ($eq $ne $gt $gte $lt $lte $like $in $nin $null):

// GET /api/users?include=posts{filter:{createdAt:{$gte:"2024-01-01"}}}
// GET /api/users?include=posts{filter:{title:{$like:"nuxt"}}}

Filtering and pagination (limit, offset) apply to to-many relations only; on a to-one relation they are a 400.

Pagination on Relations

Limit and offset related records:

// GET /api/users?include=posts{limit:5}

{
  "data": [
    {
      "id": 1,
      "name": "John Doe",
      "posts": [
        // Only first 5 posts
      ]
    }
  ]
}

With offset:

// GET /api/users?include=posts{limit:5,offset:10}

Combining Features

Combine field selection, filtering, and pagination:

// GET /api/users?include=posts[id,title]{limit:10,filter:{published:true}}

{
  "data": [
    {
      "id": 1,
      "name": "John Doe",
      "posts": [
        {
          "id": 1,
          "title": "Hello World"
          // Only id and title, only published, max 10
        }
      ]
    }
  ]
}

Deep Nesting

Combine all features with deep nesting:

// GET /api/users?include=posts[id,title]{limit:5,filter:{published:true}}.comments[id,body]{limit:3}.author[id,name]

{
  "data": [
    {
      "id": 1,
      "name": "John Doe",
      "posts": [
        {
          "id": 1,
          "title": "Hello World",
          "comments": [
            {
              "id": 1,
              "body": "Great post!",
              "author": {
                "id": 2,
                "name": "Jane Smith"
              }
            }
          ]
        }
      ]
    }
  ]
}

Multiple Relations

Include multiple relations at the same level:

// GET /api/users?include=posts[id,title],comments[id,body],profile[bio,avatar]

{
  "data": [
    {
      "id": 1,
      "name": "John Doe",
      "posts": [...],
      "comments": [...],
      "profile": {
        "bio": "...",
        "avatar": "..."
      }
    }
  ]
}

Configuration

Configure nested relations behavior in nuxt.config.ts:

export default defineNuxtConfig({
  autoApi: {
    relations: {
      // Maximum nesting depth (default: 3)
      maxDepth: 5,

      // Maximum number of relations in one request (default: 20)
      maxIncludes: 20,

      // Allow field selection (default: true)
      allowFieldSelection: true,

      // Allow filtering on relations (default: true)
      allowFiltering: true,

      // Allow pagination on relations (default: true)
      allowPagination: true,
    }
  }
})

Limits

  • Depth — at most relations.maxDepth levels (default 3). Deeper is a 400.
  • Count — at most relations.maxIncludes relations per request (default 20).
  • Relation page size — a limit inside an include above pagination.maxLimit is a 400.
  • Relations must be passed to Drizzle — ?include= uses Drizzle's relational queries, so the relations() must be in the schema you give drizzle(…, { schema }); otherwise the request fails with a 500 naming the resource.

Authorization on included relations

An included relation is authorized exactly like the resource that owns the related table:

  • Operation — the caller needs read on that resource, or the request is a 403 (401 when anonymous). A missing relation is never silently dropped: the answer to "may I include this?" is visible.
  • Row visibility — the related resource's tenant scope, listFilter, soft delete and objectLevel apply. Rows the caller may not see are removed from to-many relations and nulled on to-one relations.
  • Fields — its hidden fields and field-level read rules strip the same columns they strip from its own responses, and field selection and filters may only name its readable columns (anything else is a 400).
  • Unregistered tables — a related table that is not a registered resource (a junction table the app never exposed) is treated as part of its parent: no permission of its own, but tenant and soft-delete visibility still apply.
// 403 when the caller may not read users:
// GET /api/posts?include=author

// 400 — `password` is hidden on users, so it cannot be selected or filtered on:
// GET /api/posts?include=author[id,password]

Performance Considerations

Database Queries

Each level of nesting may result in additional database queries. The exact number depends on your database client and Drizzle's query optimization.

Indexes

For optimal performance with filtering on relations:

-- Index the fields you filter by
CREATE INDEX idx_posts_published ON posts(published);
CREATE INDEX idx_posts_created_at ON posts(created_at);

Field Selection Benefits

Selecting only needed fields reduces:

  • Network bandwidth
  • JSON serialization time
  • Client memory usage
// Good: Only fetch what you need
include=posts[id,title]

// Less efficient: Fetch everything
include=posts

Pagination Benefits

Limit relation results to avoid loading huge datasets:

// Good: Limit related records
include=posts{limit:10}

// Potentially slow: Load all posts for each user
include=posts

Common Patterns

Blog Posts with Recent Comments

GET /api/posts?include=comments[id,body,createdAt]{limit:5,filter:{createdAt:{$gte:"2024-01-01"}}}.author[name,avatar]

Users with Recent Activity

GET /api/users?include=posts[id,title]{limit:3},comments[id,body]{limit:5}

Nested Categories

GET /api/categories?include=subcategories.products[id,name,price]{limit:10}

Error Handling

GET /api/users?include=nonexistent          // 400 — Unknown relation 'nonexistent'
GET /api/users?include=posts[id,nope]       // 400 — Unknown field 'nope' in include 'posts'
GET /api/users?include=profile{limit:1}     // 400 — only to-many relations can be paginated
GET /api/users?include=a.b.c.d              // 400 — nested deeper than maxDepth
GET /api/posts?include=author               // 403 — caller may not read users

TypeScript Support

Type the included shape with the composable's type parameter:

const { data } = useAutoApiList<User & { posts: Array<Post & { comments: Comment[] }> }>('users', {
  include: 'posts.comments.author',
})

// Pass the shape you include as the type parameter
data.value?.data[0]?.posts[0]?.comments

Comparison with GraphQL

While not as flexible as GraphQL, nested relations provide similar capabilities:

FeatureNested RelationsGraphQL
Field selection✅✅
Nested loading✅✅
Filtering✅✅
Pagination✅✅
Type safety✅ (TypeScript)✅
No schema required✅❌
Learning curveLowMedium

Best Practices

  1. Always select fields: Don't load more data than needed
  2. Use pagination: Limit related records to prevent performance issues
  3. Filter early: Apply filters at the relation level when possible
  4. Monitor depth: Keep nesting to 2-3 levels for best performance
  5. Index filter fields: Create database indexes for filtered fields
  6. Cache responses: Use HTTP caching for frequently accessed nested data

Migration from Simple Includes

Existing code with simple includes continues to work:

// Old syntax (still works)
GET /api/users?include=posts,comments

// New enhanced syntax (more powerful)
GET /api/users?include=posts[id,title]{limit:10},comments[id,body]{limit:5}

The system automatically detects and handles both syntaxes.

Need a Landing Page?

Modern landing pages with optional modules (blog, docs, forms, i18n). Let's discuss your project.

Build Your MVP

Full-stack SaaS development. Expert in database design, multi-tenancy, and scalable architecture.

Deployment Help

Dockerize your backend, set up CI/CD pipelines, deploy to Cloudflare or Hetzner. Early-stage setup.

Suggest a SaaS Tool

Missing a calculator or tool? Suggest what you'd like to see on our site.