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}/tasksandPOST /projects/{projectId}/tasks - Instance Operations:
GET /projects/{projectId}/tasks/{taskId},PUT ..., andDELETE ...
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).
- The child resource is entirely dependent on the parent and cannot exist on its own (e.g.,
- Access control is inherited from the parent.
- If a user’s permission to view or edit a
Taskis derived solely from their access to the parentProject, nesting makes authorization checks naturally align with the routing hierarchy.
- If a user’s permission to view or edit a
- Child IDs are not globally unique.
- If child resources use sequential local IDs (e.g.,
Task 1,Task 2inside every project) rather than globally unique identifiers (UUIDs), the parent context in the URI is strictly required to identify the resource.
- If child resources use sequential local IDs (e.g.,
- 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).
- It provides a natural way to retrieve all related items without requiring explicit query parameters (e.g., avoiding
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
Usercan belong to multipleTeams, neither resource strictly owns the other. Root-level endpoints with relationship management (or linking resources) are more appropriate.
- If a
- 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
/tasksendpoint is needed).
- 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
- The nesting exceeds two levels of depth.
- URIs like
/projects/1/tasks/5/comments/12/attachments/3are brittle, difficult to construct, and hard to maintain. Fall back to flat instance URIs for deep resources.
- URIs like
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).
- 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.,
2. Ignoring Parent-Child Mismatches
- Anti-Pattern: Allowing
GET /projects/999/tasks/tsk_552to succeed even whentsk_552actually belongs to project101.- Why it’s bad: If your child IDs are globally unique, the backend might be tempted to ignore the
projectIdin the path and just look up the task bytaskId. This introduces massive security and authorization vulnerabilities. The server must explicitly validate that the child belongs to the provided parent.
- Why it’s bad: If your child IDs are globally unique, the backend might be tempted to ignore the
3. Duplicating Data in the Payload
- Anti-Pattern: Requiring the client to pass the
projectIdin the JSON body when sending aPOSTto/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)