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/claude-code/api-resource-granularity" ~/.claude/skills/intense-visions-harness-engineering-api-resource-granularity && rm -rf "$T"
agents/skills/claude-code/api-resource-granularity/SKILL.mdResource Granularity
Resource granularity determines how much data a single API resource exposes. Fine-grained resources are flexible but chatty; coarse-grained resources reduce round trips but over-fetch. The right granularity matches your clients' actual access patterns.
When to Use
- Designing a new API and deciding how to scope each resource
- Investigating mobile client performance issues caused by too many round trips (under-fetching)
- Debugging slow API responses caused by returning too much data (over-fetching)
- Evaluating whether to introduce a composite endpoint for a specific client use case
- Reviewing a PR that adds an endpoint returning deeply nested objects
- Deciding whether field selection (
) is worth the complexity?fields=
Instructions
Key Concepts
Fine-grained resources expose one concept per endpoint. Clients compose the data they need from multiple requests.
GET /users/42 → { id, name, email } GET /users/42/address → { street, city, zip } GET /users/42/orders → [ ... ]
Coarse-grained resources bundle related data into one response. Clients get more than they asked for, but in fewer round trips.
GET /users/42?include=address,recent-orders → { id, name, email, address: {...}, recentOrders: [...] }
The tradeoff:
| Dimension | Fine-grained | Coarse-grained |
|---|---|---|
| Round trips | Many (N+1 risk) | Few |
| Response size | Small | Large |
| Cache granularity | High (each resource cached separately) | Low (bundle invalidated as a unit) |
| Evolvability | Easy (change one resource) | Harder (bundle evolves as a whole) |
| Client fit | Generic clients | Specific client use cases |
Aggregation patterns:
-
Composite resource. A dedicated URL bundles related data for a common client need.
GET /dashboard/user/42 → user + account summary + recent activitySuitable when a single client (e.g., mobile home screen) has a fixed, known data requirement.
-
Include/expand parameter. The base resource optionally embeds related data.
GET /orders/42?expand=customer,itemsUsed by Stripe (
) to let callers control depth without separate calls.?expand[]=customer -
Field selection. Sparse fieldsets reduce payload size without changing resource boundaries.
GET /users/42?fields=id,name,emailSee
for implementation detail.api-field-selection
Worked Example
A mobile app displays a feed of posts. Each post needs author name, avatar, post text, and like count.
Fine-grained (chatty):
GET /feed → [{ postId: 1 }, { postId: 2 }, ...] GET /posts/1 → { text, authorId, likeCount } GET /users/{authorId} → { name, avatarUrl } ... (repeated for each post)
On a feed of 20 posts, this is 41 requests. Mobile latency makes this unusable.
Coarse-grained (aggregated feed resource):
GET /feed?limit=20 → { items: [ { post: { id: 1, text: "...", likeCount: 142 }, author: { id: 7, name: "Alice", avatarUrl: "..." } }, ... ], nextCursor: "eyJpZCI6MjB9" }
One request returns everything the client needs. The server joins the data efficiently; the client does not need multiple round trips.
Targeted aggregation, not wholesale bundling:
The feed resource is coarse-grained by design for its specific client. The
/posts/1 and /users/7 endpoints remain fine-grained for other callers. Do not coarsen your entire API — create targeted aggregations for specific, well-understood access patterns.
Anti-Patterns
-
The "kitchen sink" resource.
returning profile, address, orders, invoices, activity log, and preferences in one response serves no client well — it over-fetches for every one of them. Build aggregations for specific use cases, not all-purpose mega-resources.GET /users/42 -
Fine-grained resources without any aggregation option. A pure fine-grained API forces mobile clients into N+1 request chains. Always provide an aggregation path for your highest-traffic client workflows.
-
Aggregating data that changes on different cadences. If user profile rarely changes but order list changes constantly, bundling them together destroys cache effectiveness. The bundle invalidates whenever either part changes. Keep fast-changing and slow-changing data in separate cacheable resources.
-
Using granularity to hide schema complexity. If a coarse-grained endpoint returns a deeply nested structure with 30+ fields, the problem is schema complexity, not granularity. See
to let callers request only what they need.api-field-selection
Details
The N+1 Problem in REST
The N+1 problem originates in ORM query patterns (
db-n-plus-one-queries) but manifests identically in REST APIs. A list endpoint returns N items; each item requires a separate request for related data — N+1 total requests.
Mitigation strategies:
- Embed related data in the collection response (
)?expand=author - Composite resource that pre-joins the common access pattern
- Batch endpoint that accepts multiple IDs:
GET /users?ids=1,2,3,7,9
Stripe's Expand Pattern
Stripe's API lets callers control expansion depth at request time:
GET /v1/charges/ch_abc?expand[]=customer&expand[]=payment_intent
The response embeds the full customer and payment intent objects instead of returning just their IDs. This balances fine-grained defaults with coarse-grained convenience, without requiring dedicated composite endpoints for every use case.
Real-World Case Study: Mobile App Optimization
A travel booking app's hotel detail screen made 7 API calls on load: hotel info, photos, room types, availability, reviews, nearby attractions, and a policy summary. P95 load time was 4.2 seconds on 4G. The team introduced
GET /hotels/{id}/detail-bundle — a server-side join returning all 7 data sets in one response. P95 dropped to 0.8 seconds. The individual endpoints remained unchanged for other clients. The bundle is cache-keyed on the hotel ID with a 5-minute TTL; the slower-changing policy and photos data accepts stale serving.
Source
- Allamaraju, S. "RESTful Web Services Cookbook" O'Reilly (2010)
- Stripe API Expanding Responses
- Microsoft REST API Guidelines — Composite Resources
Process
- List your API's top 3-5 client workflows (e.g., home screen load, order detail page, admin dashboard). Identify the data each workflow needs.
- Check for N+1 patterns: does the workflow require a list endpoint followed by per-item requests? If so, introduce an aggregation (composite resource, expand parameter, or batch endpoint).
- Audit existing coarse-grained endpoints for over-fetching: if most callers only use a subset of the response, add field selection or split the resource.
- Ensure slow-changing and fast-changing data are in separate cacheable resources; do not bundle them.
- 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-field-selection, api-nested-vs-flat, api-pagination-cursor
Success Criteria
- High-traffic client workflows avoid N+1 request chains through aggregation, expand parameters, or batch endpoints.
- Coarse-grained aggregations are purpose-built for specific use cases, not all-purpose mega-resources.
- Fast-changing and slow-changing data are in separate cacheable resources.
- Field selection (
) or expand parameters are available on high-traffic list endpoints to control response size.?fields=