Skip to main content
API Design Mastery

Resource Modeling

Ravinder··4 min read
API DesignRESTGraphQLgRPCResource Modeling
Share:
Part 1 of 10
Resource Modeling

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/refunds

The 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_123

Reserve deep nesting for resources that genuinely cannot exist without their parent (line items of an order, for example).

graph TD A["/orders"] --> B["/orders/{id}"] B --> C["/orders/{id}/items"] B --> D["/orders/{id}/refunds"] B --> E["/orders/{id}/shipments"] C --> F["/orders/{id}/items/{itemId}"]

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/delete

Keep 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 address

This 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.
Part 1 of 10
Share: