When designing an HTTP API, how do you decide what becomes a resource? Explain why a resource is not simply a database table or a domain entity, and give an example where they differ.
answer
- resource = anything worth a URI, not a table row
- split by permission / volatility / size
- one entity → many resources; many entities → one resource
- computed and process resources have no table
- resource boundary = cache + permission + concurrency boundary
basics
~20 sA resource is anything worth naming with a URI and giving a representation — driven by what clients need to address, not by storage. Entities usually become resources, but resources also cover projections, computed views, and processes that no table matches, and one entity may back several resources.
solid answer
~50 sA **resource** is any concept that deserves its own identifier and representation from the client's point of view. An **entity** is a domain object with identity and lifecycle. They overlap heavily but are not the same mapping. The divergences that matter: - **One entity, several resources.** A `User` entity might expose `/users/{id}`, `/users/{id}/profile` (public subset), `/users/{id}/preferences` (different caching and permissions), and `/me`. - **Resources with no table.** `/reports/monthly-revenue?month=2026-07`, `/orders/42/invoice` (rendered), `/search?q=...` — computed, aggregated, or derived. - **Processes as resources.** A long-running export becomes `/exports/{id}` with a status, so the client can poll and cancel something addressable. - **Tables with no resource.** Join tables, audit rows and internal denormalizations often should not be exposed. The design question is therefore "what do clients need to name, fetch, cache and change independently?" — because a resource boundary is also a cache boundary, a permission boundary, and a concurrency boundary.
code
http · 10 linesPOST /exports HTTP/1.1
{"format":"csv","range":"2026-07"}
202 Accepted
Location: /exports/ex_7
GET /exports/ex_7 HTTP/1.1
200 OK
{"id":"ex_7","status":"running","progress":0.4}go deeper
Say a resource is anything the client needs to address by URI, give an example that is not a table (a report, a search), and note one entity can have several resources.
Use the split/merge signals — permission, volatility, size, atomicity — and give a worked example of one entity exposed as several resources.
Argue from cache, permission and concurrency boundaries, and cover process resources with 202 plus a status URI.
Treat the resource model as the durable public contract that must survive storage refactors, and reason about where composition belongs so clients never own server invariants.
## Definitions first A **resource** is the target of a URI: any information worth naming — a document, a collection, a computed value, a real-world object, a process. Its **representation** is the bytes you send for it (a JSON document, a PDF, an image); one resource can have several representations. An **entity** in the domain sense is an object with identity that persists over time — a Customer, an Order. A **table** is a storage artifact. The common beginner move is `table → resource`, one for one, CRUD on each. It works for simple systems and quietly fails as the domain grows, because storage is optimised for writes and normalisation while an API is optimised for the interactions clients actually have. ## Where the mappings diverge **One entity → many resources.** Split when parts of an entity differ in *permission*, *volatility*, or *size*. - Permission: `/users/{id}` (self and admins) versus `/users/{id}/public-profile` (anyone). - Volatility: a product's description changes monthly and its price and stock change every minute. One resource means you either cache aggressively and serve stale prices, or cache nothing and pay for the description every time. Two resources give two cache lifetimes. - Size: an article's body versus its comments; separate resources let each paginate and cache on its own. **Many entities → one resource.** A checkout page's data may come from cart, pricing, promotions and inventory. Exposing four resources forces every client into four round trips and re-implementing the join. A single `/checkouts/{id}` that composes them is a legitimate resource — the API's job is to serve the interaction, not to publish the schema. **Resources with no entity.** Computed or derived things are first-class resources: `/reports/monthly-revenue?month=2026-07`, `/accounts/{id}/balance`, `/orders/{id}/invoice` in `application/pdf`. They have URIs, representations, cache semantics and ETags like anything else. **Processes as resources.** Operations that take time are best modelled as a resource representing the operation: `POST /exports` returns `202 Accepted` with `Location: /exports/{id}`, and `GET /exports/{id}` reports `status: running|done|failed` with a link to the result. This turns "a thing happening" into something addressable, pollable, cancellable and auditable. **Entities with no resource.** Join rows, audit tables, internal caches. Exposing them leaks the schema and creates a contract you must maintain. Publish what clients need. ## Document versus collection versus controller resources A useful vocabulary: - **Document** — a single thing: `/orders/42`. - **Collection** — a server-managed set of documents: `/orders`. - **Store** — a client-managed set where the client picks the key: `PUT /users/7/bookmarks/{clientKey}`. - **Controller** — a resource representing an executable concept when no document/collection mapping is honest. Recognising which kind you are creating keeps the verbs consistent: collections take `POST` to create and `GET` to list; documents take `GET`/`PUT`/`PATCH`/`DELETE`; stores take client-keyed `PUT`. ## Granularity signals in practice Split a resource when: parts have different permissions; parts have very different change rates; part of it is unbounded (a growing list) and would make the parent's representation grow without limit; clients routinely want one part without the other; or two parts need independent optimistic-concurrency control, because a single ETag over a whole aggregate makes unrelated edits collide. Merge when: clients always fetch the pieces together; the pieces change together and share a lifecycle; or splitting would force clients to reassemble an invariant the server should own (never make a client responsible for keeping two resources consistent — if updating them together must be atomic, they are one resource). ## A worked example A `Subscription` entity in the database has plan, price, status, card token, billing address, and a usage counter. As an API: - `/subscriptions/{id}` — plan, status, dates. Moderately cacheable, owner-readable. - `/subscriptions/{id}/payment-method` — separate permission, separate audit, PCI-sensitive, changed by a different flow. - `/subscriptions/{id}/usage` — high-volatility counter, short cache, cheap to fetch alone. - `/subscriptions/{id}/invoices` — an unbounded collection, paginated, would otherwise inflate the parent forever. One table, four resources — each with its own cache policy, permission and concurrency token. That is the mapping doing real work rather than mirroring storage.
- Give a concrete signal that one resource should be split into two.Different change rates with a shared cache policy. If a product's description is stable for months but its stock count changes every few seconds, one resource forces a cache lifetime that is either wrong for the price or wasteful for the description. Different permissions are the other strong signal — if half the fields need an elevated role, that half is a separate resource.
- When is it wrong to split, even though the pieces look separable?When they must change atomically. If a client updating A without B leaves the system in an invalid state, splitting makes the client responsible for an invariant the server should own — and HTTP gives you no cross-resource transaction. Keep them in one resource so a single request carries the whole consistent change.
- Is exposing exactly one endpoint per database table ever the right call?For a small internal CRUD service with one known client, it is fast and adequate. It becomes a liability when the schema is your public contract: every normalization or refactor becomes a breaking API change, and clients pay round trips to re-join what you split. Even then, a thin projection layer is cheap insurance.
A restaurant menu is not the kitchen's inventory list: it names what diners order, sometimes combining ingredients and sometimes hiding them entirely.
saying these in an interview costs you the question
- Equating resources with database tables and generating one CRUD endpoint per table
- Treating a resource as a class and its endpoints as methods on that class
- Believing a resource must map to something stored — rejecting computed or process resources
- Splitting resources that must change atomically, pushing an invariant onto the client
- Never splitting, so one endpoint carries fields with wildly different permissions and cache lifetimes