git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/api-nested-vs-flat" ~/.claude/skills/intense-visions-harness-engineering-api-nested-vs-flat-1894f3 && rm -rf "$T"
agents/skills/codex/api-nested-vs-flat/SKILL.mdNested vs Flat Resource URLs
Nested URLs express ownership hierarchies; flat URLs with query parameters express arbitrary membership or filtering. The decision affects URL stability, caching, access control, and client complexity.
When to Use
- Designing a URL structure for a resource that belongs to a parent (comments on a post, items in an order)
- Deciding whether a resource should live at
or/parents/{id}/children/children?parentId={id} - Reviewing a PR that adds a deeply nested route (three or more levels)
- Evaluating URL stability when a resource might be re-parented or accessed in multiple contexts
- Explaining to a team why
is problematic/users/42/posts/7/comments/3/likes
Instructions
Key Concepts
Nested URLs encode the parent-child relationship in the path:
GET /users/42/posts GET /users/42/posts/7/comments
Flat URLs with filters move the parent relationship to a query parameter:
GET /posts?userId=42 GET /comments?postId=7
Decision criteria:
| Signal | Prefer Nested | Prefer Flat |
|---|---|---|
| Resource cannot exist without parent | Yes (comment without post) | No |
| Resource has only one parent type | Yes | No (if multiple parent types) |
| Client always knows the parent ID | Yes | Depends |
| Resource is accessed in multiple parent contexts | No | Yes |
| URL depth would exceed 2 levels | No | Yes |
| Access control is scoped to parent | Yes (URL structure aids enforcement) | No |
The two-level rule: Nest a maximum of two levels.
/users/42/posts is acceptable. /users/42/posts/7/comments is at the limit. /users/42/posts/7/comments/3/likes is too deep — flatten it.
Flattening deep hierarchies:
# Deep (avoid) GET /users/42/posts/7/comments/3/likes # Flat (prefer) GET /likes?commentId=3 # Or: flat canonical URL with nested alias GET /comments/3/likes (nested, 2 levels from canonical comment resource)
Worked Example
A blogging platform has posts, comments, and likes on comments.
Draft 1 — fully nested:
GET /users/42/posts → user's posts GET /users/42/posts/7 → single post POST /users/42/posts/7/comments → add comment GET /users/42/posts/7/comments → post's comments POST /users/42/posts/7/comments/3/likes → like a comment GET /users/42/posts/7/comments/3/likes → comment's likes
Problem: to fetch comment 3's likes, the client must know the user ID (42), post ID (7), and comment ID (3). If the comment is later moved to a different post, every URL breaks. The client must carry the full ancestry chain.
Draft 2 — two-level nesting with flat deep resources:
GET /posts?authorId=42 → filter by author (flat) GET /posts/7 → canonical post URL (flat) POST /posts/7/comments → comments on a post (1 level nesting, OK) GET /posts/7/comments → comments on a post POST /comments/3/likes → likes on a comment (1 level nesting, OK) GET /comments/3/likes → comment's likes
Likes also get a canonical flat address:
GET /likes?commentId=3 → same data, filterable
The client no longer needs the post's parent user to fetch a comment's likes. Comment 3 has a canonical URL (
/comments/3) that is stable even if the comment moves to a different post.
Access control with nested URLs:
Nested URLs make scope-based access control natural. Middleware can extract the parent ID from the path and enforce ownership before the handler runs:
GET /organizations/org-7/projects/proj-12/members
The middleware verifies the requester belongs to
org-7 before checking project membership. The ownership chain is explicit in the URL. This is a genuine benefit of nesting.
Anti-Patterns
-
Deep nesting beyond two levels. Each additional level makes URLs brittle (break on re-parenting), harder to cache, and harder to construct for clients.
is a symptom of modeling the database schema rather than the access patterns./a/{id}/b/{id}/c/{id}/d/{id} -
Duplicating the same resource under multiple parents. If comments are accessible at both
and/posts/7/comments/3
, you have two canonical URLs for the same resource. Clients and caches disagree on staleness. Pick one canonical URL; use the other as an alias with a redirect if needed./articles/7/comments/3 -
Nesting resources that can have multiple parent types. A file attached to a message, a project, and an invoice should live at
with filters (/files/{id}
,?messageId=
), not nested under each parent type.?projectId= -
Using nested URLs to express filtering.
— is/users/42/orders/active
a sub-resource or a filter? If it is a filter, useactive
. Reserve nesting for genuine ownership relationships./orders?userId=42&status=active
Details
When Flat Wins: The Multi-Parent Problem
GitHub's pull request reviews illustrate the tradeoff:
GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews
This is deep (4 levels) but each ancestor is required context: you cannot review a pull request without knowing the repo and owner. The full ancestry is always available to the client. GitHub accepts the depth because the parent chain is always known and stable.
Contrast with tags, which can belong to issues, pull requests, or releases:
# What GitHub does NOT do: GET /repos/{owner}/{repo}/issues/{issue_number}/labels (nested) GET /repos/{owner}/{repo}/labels (flat — label registry)
Labels are fetched from the issue context when reviewing an issue, but the label registry is flat. Multi-parent resources that need to be listed independently belong at a flat URL.
Canonical URL and Aliases
A resource should have one canonical URL. Nested URLs can serve as scoped aliases that redirect to the canonical form:
GET /posts/7/comments/3 → 301 Moved Permanently Location: /comments/3
Or serve the same response from both paths and set
Content-Location: /comments/3 to signal the canonical address. This keeps nested URLs useful for navigation while ensuring cache consistency.
Source
- Masse, M. "REST API Design Rulebook" O'Reilly (2011)
- Microsoft REST API Guidelines — URL Structure
- GitHub REST API Docs — practical examples of nesting vs flat
- RFC 3986 — URI Generic Syntax
Process
- For each resource, ask: "Can this resource exist without its parent?" If no, nesting is appropriate. If yes, flat with filter parameters is safer.
- Count nesting depth. If the URL exceeds 2 levels, flatten deeper children to their own top-level collection with filter parameters.
- Check for multi-parent resources. If a resource belongs to more than one parent type, use a flat canonical URL with filter parameters.
- Define a canonical URL for every resource. Nested aliases may exist but must redirect to or agree with the canonical.
- Run
to confirm skill files are well-formed.harness validate
Harness Integration
- Type: knowledge -- this skill is a reference document, not a procedural workflow.
- No tools or state -- consumed as context by other skills and agents.
- related_skills: api-resource-modeling, api-filtering-sorting, api-resource-granularity, api-rest-maturity-model
Success Criteria
- URL nesting does not exceed two levels for any resource.
- Resources that can exist independently of their parent have a canonical flat URL.
- Multi-parent resources use flat URLs with query parameter filters.
- Every resource has exactly one canonical URL; nested aliases redirect to the canonical form.