Harness-engineering api-nested-vs-flat

Nested vs Flat Resource URLs

install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
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"
manifest: agents/skills/codex/api-nested-vs-flat/SKILL.md
source content

Nested 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
    /parents/{id}/children
    or
    /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
    /users/42/posts/7/comments/3/likes
    is problematic

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:

SignalPrefer NestedPrefer Flat
Resource cannot exist without parentYes (comment without post)No
Resource has only one parent typeYesNo (if multiple parent types)
Client always knows the parent IDYesDepends
Resource is accessed in multiple parent contextsNoYes
URL depth would exceed 2 levelsNoYes
Access control is scoped to parentYes (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

  1. Deep nesting beyond two levels. Each additional level makes URLs brittle (break on re-parenting), harder to cache, and harder to construct for clients.

    /a/{id}/b/{id}/c/{id}/d/{id}
    is a symptom of modeling the database schema rather than the access patterns.

  2. Duplicating the same resource under multiple parents. If comments are accessible at both

    /posts/7/comments/3
    and
    /articles/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.

  3. Nesting resources that can have multiple parent types. A file attached to a message, a project, and an invoice should live at

    /files/{id}
    with filters (
    ?messageId=
    ,
    ?projectId=
    ), not nested under each parent type.

  4. Using nested URLs to express filtering.

    /users/42/orders/active
    — is
    active
    a sub-resource or a filter? If it is a filter, use
    /orders?userId=42&status=active
    . Reserve nesting for genuine ownership relationships.

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

Process

  1. For each resource, ask: "Can this resource exist without its parent?" If no, nesting is appropriate. If yes, flat with filter parameters is safer.
  2. Count nesting depth. If the URL exceeds 2 levels, flatten deeper children to their own top-level collection with filter parameters.
  3. Check for multi-parent resources. If a resource belongs to more than one parent type, use a flat canonical URL with filter parameters.
  4. Define a canonical URL for every resource. Nested aliases may exist but must redirect to or agree with the canonical.
  5. Run
    harness validate
    to confirm skill files are well-formed.

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.