Resource Modeling
Series
API Design MasteryPart 2 →
Versioning Strategies
Most API design mistakes trace back to one root cause: the designer was thinking in procedures, not resources. You end up with endpoints like /getUser, /createOrder, /processPayment — a collection of remote function calls dressed up as a web API. Clients couple to your internal operations, versioning becomes a nightmare, and every new workflow means a new endpoint. Resource modeling is the discipline that prevents this.
Why Nouns Beat Verbs
HTTP already has verbs: GET, POST, PUT, PATCH, DELETE. When you put a verb in your URL, you are doubling up — the method POST /createOrder is redundant and conflicting. POST /orders expresses the same intent cleanly.
The noun-based approach aligns with how HTTP was designed. Resources are addressable things. Operations on them are expressed through methods and representations, not URL tokens.
# Bad — verb-based
POST /createOrder
GET /getOrderById?id=42
POST /cancelOrder
POST /processRefund
# Good — resource-based
POST /orders
GET /orders/42
DELETE /orders/42
POST /orders/42/refundsThe second group is predictable. Any developer can guess the shape of the API before reading a single line of documentation.
Resource Hierarchy and Nesting
Resources naturally relate to each other. A pragmatic rule: nest up to two levels deep, then stop.
/users/{userId}
/users/{userId}/addresses
/users/{userId}/addresses/{addressId}Beyond two levels, URLs become brittle. If an address can exist independently, give it a top-level resource and use query parameters to filter:
GET /addresses?userId=u_123Reserve deep nesting for resources that genuinely cannot exist without their parent (line items of an order, for example).
Naming Conventions
Consistency matters more than any particular style. Pick a convention and enforce it with a linter.
| Concern | Recommendation | Example |
|---|---|---|
| Case | lowercase kebab-case | /payment-methods |
| Pluralization | plural nouns | /users, /orders |
| IDs | path parameter | /users/{userId} |
| Actions (unavoidable) | sub-resource or verb under resource | /orders/{id}/cancel |
Some operations have no clean noun equivalent — bulk deletes, state transitions, async kicks. Use a sub-resource named after the action as a last resort:
POST /orders/42/cancellation
POST /batch/deleteKeep it rare. Every verb in a URL is a design smell worth reconsidering.
Representing State and Sub-Resources
State transitions should not be modeled as separate mutation endpoints. Model the state as a field on the resource and update it with PATCH:
PATCH /orders/42 HTTP/1.1
Content-Type: application/json
{
"status": "cancelled"
}When the transition itself has semantics (confirmation emails, payment reversal), make the outcome a resource:
POST /orders/42/cancellations HTTP/1.1
Content-Type: application/json
{
"reason": "customer_request",
"refundPolicy": "full"
}The response is a Cancellation resource with its own ID, timestamps, and audit trail. This is more powerful than a bare state mutation because the event is addressable.
ID Strategy
Avoid sequential integers in public APIs. They leak business intelligence (how many orders you have) and make enumeration trivial.
{
"id": "ord_01J8K2MNPQ3RS4TUV5WX6YZ7A",
"createdAt": "2025-10-01T09:00:00Z"
}Prefixed ULIDs (like Stripe's ord_ prefix) give you:
- Sortability by creation time
- Type safety (a prefix mismatch is immediately obvious)
- No guessable sequence
Encode the resource type in the prefix. Debugging a log full of ord_, usr_, pmt_ IDs is far easier than a sea of UUIDs.
Practical Example: Modeling an E-Commerce API
GET /products # list catalog
GET /products/{productId} # product detail
GET /products/{productId}/variants # size/color variants
POST /carts # create cart
POST /carts/{cartId}/items # add item
PATCH /carts/{cartId}/items/{itemId} # update quantity
DELETE /carts/{cartId}/items/{itemId} # remove item
POST /orders # checkout (creates from cart)
GET /orders/{orderId} # order status
POST /orders/{orderId}/cancellations # cancel order
GET /users/{userId}/addresses # saved addresses
POST /users/{userId}/addresses # add addressThis structure can be documented, typed, and consumed by generated clients without any extra ceremony.
Key Takeaways
- Use HTTP methods as your verbs; reserve URL tokens for resource names (nouns).
- Nest resources at most two levels deep; use query parameters for filtering at deeper levels.
- Enforce a single naming convention — lowercase plural kebab-case is the most common and safest choice.
- Model state transitions as sub-resources when the transition itself carries business meaning.
- Use prefixed, non-sequential IDs (ULID or UUIDv7) to avoid leaking business data and to aid debugging.
- When you feel forced to put a verb in a URL, treat it as a signal to revisit the resource model, not a green light to proceed.
Series
API Design MasteryPart 2 →
Versioning Strategies