28. Pattern: Nested Resource Lifecycle

The Nested Resource Lifecycle pattern structures API endpoints by placing a child resource collection logically underneath a specific parent resource instance (e.g., /projects/1/tasks).

This pattern extends standard RESTful CRUD capabilities to dependent resources, inherently scoping all operations—creation, retrieval, updating, and deletion—to the context of the parent. It clearly communicates strict ownership and composition within your domain model directly through the URI structure.

28.1. Overview

In many domain models, certain entities cannot exist independently. A Task only makes sense within the context of a Project. A LineItem only exists as part of an Invoice.

When designing APIs for these relationships, flattening all resources at the root level (e.g., creating a task via POST /tasks with a projectId in the payload) can strip away valuable context, complicate access control, and make the API less intuitive.

The Nested Resource Lifecycle pattern addresses this by anchoring the child collection to a specific parent instance:

  • Collection Operations: GET /projects/{projectId}/tasks and POST /projects/{projectId}/tasks
  • Instance Operations: GET /projects/{projectId}/tasks/{taskId}, PUT ..., and DELETE ...

By routing through the parent, the API implicitly guarantees that the child resource belongs to that parent, simplifying backend validation and providing a clear, hierarchical mental model for the API consumer.

28.2. When to Use Nested Resource Lifecycle

Use the Nested Resource Lifecycle pattern when:

  • There is a strict composition or ownership relationship.
    • The child resource is entirely dependent on the parent and cannot exist on its own (e.g., /articles/{articleId}/comments).
  • Access control is inherited from the parent.
    • If a user’s permission to view or edit a Task is derived solely from their access to the parent Project, nesting makes authorization checks naturally align with the routing hierarchy.
  • Child IDs are not globally unique.
    • If child resources use sequential local IDs (e.g., Task 1, Task 2 inside every project) rather than globally unique identifiers (UUIDs), the parent context in the URI is strictly required to identify the resource.
  • You need to filter collections implicitly by parent.
    • It provides a natural way to retrieve all related items without requiring explicit query parameters (e.g., avoiding GET /tasks?projectId=123).

28.3. When NOT to Use Nested Resource Lifecycle

Avoid the Nested Resource Lifecycle pattern when:

  • The resources have a many-to-many relationship or are shared.
    • If a User can belong to multiple Teams, neither resource strictly owns the other. Root-level endpoints with relationship management (or linking resources) are more appropriate.
  • The child needs to be queried across multiple parents.
    • If clients frequently need to fetch “all tasks assigned to me across all projects,” forcing them to query through individual projects is highly inefficient. (In this case, a root /tasks endpoint is needed).
  • The nesting exceeds two levels of depth.
    • URIs like /projects/1/tasks/5/comments/12/attachments/3 are brittle, difficult to construct, and hard to maintain. Fall back to flat instance URIs for deep resources.

28.4. What the Pattern Looks Like

Below are HTTP request and response flows demonstrating the lifecycle of a nested Task resource under a Project.

1. Creating a Nested Resource

The client creates a new task within a specific project. The parent ID (prj_101) is inferred from the URI, meaning the client does not need to explicitly provide a projectId in the request body.

Request

POST /projects/prj_101/tasks HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json

{
  "title": "Finalize database schema",
  "assignee": "usr_789"
}

Response

The server successfully creates the resource and returns a 201 Created status with the Location header pointing to the fully nested URI.

HTTP/1.1 201 Created
Location: https://api.example.com/projects/prj_101/tasks/tsk_552
Content-Type: application/json

{
  "id": "tsk_552",
  "projectId": "prj_101",
  "title": "Finalize database schema",
  "assignee": "usr_789",
  "status": "pending",
  "createdAt": "2026-09-25T14:33:14Z"
}

2. Retrieving the Nested Collection

The client fetches all tasks for the specific project.

Request

GET /projects/prj_101/tasks?status=pending HTTP/1.1
Host: api.example.com

Response

HTTP/1.1 200 OK
Content-Type: application/json

{
  "items": [
    {
      "id": "tsk_552",
      "title": "Finalize database schema",
      "status": "pending"
    },
    {
      "id": "tsk_553",
      "title": "Setup CI/CD pipeline",
      "status": "pending"
    }
  ],
  "total": 2
}

28.5. Anti-Patterns to Avoid

1. The “URL Train” (Over-Nesting)

  • Anti-Pattern: GET /companies/1/departments/4/projects/12/tasks/55
    • Why it’s bad: Deeply nested URIs become rigid and difficult for clients to assemble. They also tightly couple the API design to a specific UI navigation path. Limit nesting to one level (Parent -> Child). For deeper interactions, use flat endpoints (e.g., GET /tasks/55/comments).

2. Ignoring Parent-Child Mismatches

  • Anti-Pattern: Allowing GET /projects/999/tasks/tsk_552 to succeed even when tsk_552 actually belongs to project 101.
    • Why it’s bad: If your child IDs are globally unique, the backend might be tempted to ignore the projectId in the path and just look up the task by taskId. This introduces massive security and authorization vulnerabilities. The server must explicitly validate that the child belongs to the provided parent.

3. Duplicating Data in the Payload

  • Anti-Pattern: Requiring the client to pass the projectId in the JSON body when sending a POST to /projects/{projectId}/tasks.
    • Why it’s bad: It forces the server to handle conflict resolution if the URI ID and the payload ID do not match. The URI should act as the authoritative source of truth for the parent context.

28.6. OpenAPI Example

A complete OpenAPI 3.0.3 specification illustrating a nested resource collection with GET and POST methods.

openapi: 3.0.3
info:
  title: Project Tasks API - Nested Resource Lifecycle
  version: 1.0.0
servers:
  - url: https://api.example.com

paths:
  /projects/{projectId}/tasks:
    parameters:
      - in: path
        name: projectId
        required: true
        schema:
          type: string
          example: "prj_101"
        description: The unique identifier of the parent project.
        
    get:
      summary: List tasks for a project
      description: Returns a collection of tasks belonging to the specified project.
      tags: [Tasks]
      responses:
        '200':
          description: A list of tasks
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskListResponse'
                
    post:
      summary: Create a task within a project
      description: Creates a new task. The task is inherently bound to the project specified in the path.
      tags: [Tasks]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaskCreateRequest'
      responses:
        '201':
          description: Task successfully created
          headers:
            Location:
              schema:
                type: string
              description: URI of the newly created task
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'

components:
  schemas:
    TaskCreateRequest:
      type: object
      required:
        - title
      properties:
        title:
          type: string
          example: "Finalize database schema"
        assignee:
          type: string
          example: "usr_789"
          
    Task:
      type: object
      properties:
        id:
          type: string
          example: "tsk_552"
        projectId:
          type: string
          example: "prj_101"
        title:
          type: string
          example: "Finalize database schema"
        status:
          type: string
          example: "pending"
          
    TaskListResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Task'
        total:
          type: integer
          example: 2

28.7. Visualizing Nested Resource Lifecycle (Mermaid Diagram)

sequenceDiagram
    autonumber
    actor Client
    participant API as API Server
    participant DB as Database

    Note over Client,DB: Creating a Nested Resource
    Client->>API: POST /projects/prj_101/tasks (Body: { title: "..." })
    
    API->>API: Extract 'prj_101' from path
    API->>DB: Verify 'prj_101' exists & User has permission
    
    DB-->>API: Project valid
    API->>DB: INSERT INTO tasks (project_id, title) VALUES ('prj_101', '...')
    DB-->>API: Returns new task ID (tsk_552)
    
    API-->>Client: 201 Created (Location: /projects/prj_101/tasks/tsk_552)
    
    Note over Client,DB: Retrieving the Nested Resource
    Client->>API: GET /projects/prj_101/tasks/tsk_552
    API->>DB: SELECT * FROM tasks WHERE id='tsk_552' AND project_id='prj_101'
    DB-->>API: Task Data
    API-->>Client: 200 OK (Task Payload)