Designing Resources
Modeling resources and URIs (nouns, not verbs), the conventions for pagination, filtering, and sorting, and shaping representations that don't leak your database.
On this page
Getting the verbs right (last lesson) is half of REST; the other half is modeling the resources those verbs act on. Good resource design makes an API feel obvious - a developer guesses the URL correctly on the first try. Bad design leaks your database schema, buries actions in query strings, and forces consumers to memorize idiosyncrasies. The craft is choosing the right nouns, shaping representations that hide your internals, and following the boring conventions for collections.
Resources are nouns; hierarchy shows relationships
A resource is a thing the API exposes - an account, a transaction, an invoice. URLs name these nouns, and nesting expresses containment:
/accounts collection of accounts
/accounts/42 a single account
/accounts/42/transactions the transactions belonging to account 42 (a sub-collection)
/accounts/42/transactions/9 one specific transaction of that accountConventions that keep this predictable:
- Plural nouns for collections (
/accounts, not/account) - consistency beats grammar debates. - Nest to show relationship, but not too deep.
/accounts/42/transactionsis clear; nesting four levels (/banks/1/branches/2/accounts/42/transactions/9) is painful - once a resource has its own stable id, prefer/transactions/9over deep nesting. - No verbs (last lesson) - the action is the HTTP method.
The awkward case: actions that aren't CRUD
Not everything is create/read/update/delete. "Freeze this account," "transfer money," "retry this job" are operations, and forcing them into pure CRUD is unnatural. Two accepted approaches:
- Model the operation as a resource. A transfer is a thing:
POST /transferswith a body describing it, returning aTransferyou can laterGET. This is often the cleanest - the operation becomes a first-class, inspectable resource. - A controller sub-resource for state transitions:
POST /accounts/42/freeze. Pragmatic and readable for simple state changes; use it sparingly and consistently.
Prefer modeling operations as resources when they have their own data or lifecycle; the sub-resource verb is fine for simple, stateless transitions.
Collection conventions: paging, filtering, sorting
Any collection large enough to matter must never return everything at once (recall the batch/streaming lessons - unbounded results kill servers and clients). Use consistent query parameters:
GET /transactions?accountId=42&status=SETTLED β filtering
GET /transactions?sort=-createdAt β sorting (- for descending)
GET /transactions?page=2&size=50 β pagination (or cursor-based)- Filtering narrows a collection via query params - these are not new resources, just views of one.
- Sorting via a
sortparam; a leading-for descending is a common convention. - Pagination is mandatory for large collections. Offset pagination (
page/size) is simple but drifts if data changes between pages; cursor pagination (an opaque?after=<cursor>) is stable and scales better for large or live datasets. Return metadata (total count or a next-page link) so clients can navigate.
Representations should not leak your database
The JSON you return is a representation you design deliberately - it is not your JPA entity serialized:
// β leaking the entity: exposes internal column names, an internal FK, a nullable audit field
{ "acct_id": 42, "bal": 100.00, "usr_fk": 7, "internal_risk_flag": null, "db_version": 3 }
// β
a designed DTO: consumer-friendly names, only what clients should see, computed fields
{ "id": 42, "balance": "100.00", "currency": "USD", "status": "ACTIVE",
"ownerId": 7, "createdAt": "2026-07-22T10:00:00Z" }Serializing entities directly couples your public contract to your schema (rename a column, break every client), exposes fields consumers shouldn't see, and leaks internal concepts. Map to a purpose-built response DTO at the boundary - the same "map at the edge" discipline from the hexagonal module, now protecting your API contract.
Design money and dates carefully - they're contract landmines
Represent money as a string or integer minor units ('100.00' or 10000 cents), never a floating-point number - JSON floats lose precision and clients disagree on rounding. Always include the currency. Use ISO-8601 with a timezone (UTC) for timestamps, never a raw epoch or a local-time string. These small representation choices are part of the contract, and getting them wrong causes subtle, expensive bugs across every consumer.
Your database is the stockroom: shelves labeled with cryptic SKUs, internal reorder flags, supplier codes - organized for your operations. The API representation is the shop window: a deliberately arranged display showing customers exactly what they need in an appealing, understandable form, hiding the loading dock and the inventory system. A shop that just wheeled its stockroom shelves to the front (serializing entities) confuses customers with internal codes and exposes things they shouldn't see - and every time you reorganize the stockroom, the 'display' breaks. You design the window on purpose, independently of how the stockroom happens to be arranged.
Design the API for this feature: users have wallets, wallets contain transactions, and a user can initiate a 'top-up' (adding funds, which is a multi-step operation with its own status). Also, the transactions list can be huge. Sketch the URLs and verbs for: listing a wallet's transactions (filtered by status, paginated), fetching one transaction, and initiating a top-up. Note one representation choice you'd make carefully.
Why should an API return a designed DTO rather than serializing your JPA entity directly?
Key takeaways
- Resources are nouns; URL hierarchy shows relationships (/accounts/42/transactions) - use plural collections and avoid over-deep nesting.
- For non-CRUD operations, model the operation as a resource (POST /transfers) or use a controller sub-resource for simple state changes (POST /accounts/42/freeze).
- Large collections must be paginated (offset or, better, cursor) and support consistent filtering and sorting query params - never return everything.
- The API representation is a deliberately designed DTO, not your serialized entity - protecting the contract from schema changes and hiding internal fields.
- Representation details are part of the contract: money as string/minor-units with a currency (never a float), timestamps as ISO-8601 UTC.