Overview
Resource modeling is the process of designing an HTTP API around concepts that clients can identify, retrieve, create, change, and delete. REST uses a uniform interface: resource identifiers, representations, standard HTTP methods, status codes, headers, caching rules, and stateless requests.
A resource is not necessarily a database row. It can represent:
- A business entity such as an order.
- A collection such as all orders visible to a user.
- A relationship such as an order's shipments.
- A workflow state such as a payment attempt.
- A computed result such as a price quote.
- A long-running operation.
Good resource design gives clients a stable business-facing contract without exposing internal tables, services, or object graphs.
Not every business operation maps naturally to CRUD. Commands such as approving a loan, capturing a payment, retrying a failed job, or calculating a route may be clearer as RPC-style endpoints. RPC-style HTTP is acceptable when it exposes an explicit business operation and still uses HTTP semantics honestly.
This topic matters in interviews because candidates must demonstrate more than route naming. They should understand:
- Resources versus representations.
- Collection and item semantics.
- Safe and idempotent methods.
POST,PUT, andPATCHtrade-offs.- Status codes, headers, caching, and conditional requests.
- Asynchronous operations.
- When an action endpoint is clearer than inventing a misleading resource.
- How API contracts remain independent of persistence and domain internals.
Core Concepts
Resource, Representation, and Identifier
A resource is the conceptual thing exposed by the API. A representation is the current serialized form sent in a request or response. A URI identifies the resource.
GET /orders/ord_123 HTTP/1.1
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "ord_123",
"status": "pendingPayment",
"total": {
"amount": 125.00,
"currency": "USD"
}
}
The JSON document is not the resource itself. It is one representation of the order at a point in time.
This distinction permits:
- Different media types.
- Different language representations.
- Versioned representations.
- Partial or summary views.
- Caching based on representation metadata.
Model Business Resources, Not Database Tables
An API is a contract for clients, not a remote database.
Avoid exposing:
/tbl_orders
/order_status_lookup
/order_line_join
Prefer business concepts:
/orders
/orders/{orderId}
/orders/{orderId}/lines
/orders/{orderId}/shipments
Table-shaped APIs often leak:
- Internal normalization.
- Surrogate keys that have no client meaning.
- Join tables.
- Persistence terminology.
- Fields clients should not control.
Use request and response models tailored to the API contract rather than serializing ORM entities directly.
Resource Granularity
Resources should be large enough to support useful client operations and small enough to avoid excessive transfer and coupling.
Too fine-grained:
GET /orders/123/status
GET /orders/123/total
GET /orders/123/customer-name
This creates chatty APIs and many network round trips.
Too coarse:
GET /customer-account-everything/123
This transfers unrelated data, complicates authorization, and couples clients to one large representation.
Choose boundaries from:
- Client use cases.
- Business ownership.
- Consistency requirements.
- Security boundaries.
- Change patterns.
- Payload and latency constraints.
Collection and Item Resources
A collection and an item are separate resources:
/orders
/orders/{orderId}
Typical semantics:
GET /orders
POST /orders
GET /orders/ord_123
PUT /orders/ord_123
PATCH /orders/ord_123
DELETE /orders/ord_123
The API does not need to support every method on every resource. Expose only operations that match the business and authorization model.
URI Design
Useful conventions include:
- Use nouns for resources.
- Use plural nouns for collections.
- Use stable opaque identifiers.
- Use lowercase paths consistently.
- Keep nesting shallow.
- Put optional query behavior in the query string.
- Avoid leaking implementation technology.
Good examples:
/customers/cus_42
/customers/cus_42/orders
/orders?status=pending&sort=-createdAt
Avoid:
/getCustomerById?id=42
/sql/orders/42
/customers/42/orders/99/lines/5/product/category
Deep nesting is difficult to maintain. Once an item has a stable identity, a top-level item URI is often clearer.
Stateless Requests
REST requests should contain the information required to understand and authorize them. The server should not rely on hidden conversational state tied to one server instance.
Statelessness improves:
- Horizontal scaling.
- Retry behavior.
- Load balancing.
- Failure recovery.
- Observability.
State still exists in resources, tokens, databases, and workflows. Stateless means the protocol request is independently understandable, not that the system stores no state.
HTTP Method Semantics
HTTP methods carry standardized meaning:
Safe means the client is not asking for a state change. Logging and metrics can still occur.
Idempotent means repeating the same request has the same intended effect on resource state. Responses can differ. A repeated DELETE can return 204 first and 404 later while remaining idempotent.
GET and HEAD
GET retrieves a representation and must not be used to request a business mutation:
GET /orders/123/cancel
is dangerous because browsers, crawlers, prefetchers, and caches can issue GET.
Use HEAD when clients need the same metadata as GET without response content:
HEAD /documents/doc_123
Useful response metadata includes:
Content-Length.Content-Type.ETag.Last-Modified.- Cache headers.
POST
Use POST when:
- The server assigns the new resource URI.
- The target collection processes a creation request.
- The operation is not naturally idempotent.
- A command does not fit replacement semantics.
- A request creates a subordinate or operation resource.
POST /orders HTTP/1.1
Content-Type: application/json
{
"customerId": "cus_42",
"lines": [
{
"productId": "prd_7",
"quantity": 2
}
]
}
Successful creation:
HTTP/1.1 201 Created
Location: /orders/ord_123
Content-Type: application/json
{
"id": "ord_123",
"status": "draft"
}
POST can return 200 when it processes a request and returns a result without creating a resource. Use 202 Accepted for deferred processing.
PUT
PUT requests that the target resource state be created or replaced by the supplied representation.
PUT /profiles/usr_42/preferences HTTP/1.1
Content-Type: application/json
{
"theme": "dark",
"locale": "en-US"
}
Repeating the same request produces the same intended state, making PUT idempotent.
Important considerations:
- The client knows the target URI.
- Omitted fields can imply removal or default values under replacement semantics.
- The API must define whether creation at the URI is allowed.
- Return
200with a representation,204without one, or201if created.
Do not call an arbitrary merge update PUT while silently preserving omitted properties. That makes the contract ambiguous.
PATCH
PATCH applies a partial modification. The media type defines patch semantics.
JSON Merge Patch expresses a partial document:
PATCH /customers/cus_42 HTTP/1.1
Content-Type: application/merge-patch+json
{
"displayName": "A. Nguyen",
"phone": null
}
JSON Patch expresses ordered operations:
PATCH /customers/cus_42 HTTP/1.1
Content-Type: application/json-patch+json
[
{
"op": "replace",
"path": "/displayName",
"value": "A. Nguyen"
}
]
PATCH is not automatically idempotent. Replacing a value can be idempotent; incrementing a value is not.
Validate:
- Allowed paths.
- Authorization per field.
- Resulting resource invariants.
- Preconditions such as
If-Match. - Patch document size and operation count.
DELETE
DELETE requests removal of the association between the target URI and its current functionality. Business systems may implement:
- Hard deletion.
- Soft deletion.
- Deactivation.
- Retention workflow.
The external semantics must be clear. If cancellation is a meaningful state transition rather than deletion, use a cancellation operation instead.
Common responses:
204 No Contentwhen removal succeeds.202 Acceptedwhen deletion is asynchronous.404 Not Foundwhen no visible resource exists.409 Conflictwhen current state prevents deletion.
Response Status Codes
Choose codes from HTTP semantics, not framework convenience.
Common success codes:
200 OK: successful request with a response representation.201 Created: resource created; includeLocation.202 Accepted: processing accepted but not complete.204 No Content: successful request with no response content.
Common client-error codes:
400 Bad Request: malformed syntax or invalid request shape.401 Unauthorized: authentication is required or invalid.403 Forbidden: authenticated client lacks permission.404 Not Found: target resource is unavailable or intentionally hidden.405 Method Not Allowed: method unsupported for the target; includeAllow.409 Conflict: request conflicts with current resource state.412 Precondition Failed: conditional request precondition failed.415 Unsupported Media Type: request content type is unsupported.422 Unprocessable Content: syntactically valid content cannot be processed semantically.429 Too Many Requests: rate limit exceeded.
Server errors:
500 Internal Server Error: unexpected server failure.502 Bad Gateway: invalid upstream response.503 Service Unavailable: temporary unavailability.504 Gateway Timeout: upstream timeout.
Do not return 200 OK with an error object for failures. Clients, gateways, monitoring, and retry policies rely on status semantics.
Error Representations
Use one consistent machine-readable error shape. Problem Details is a standard format:
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/order-already-shipped",
"title": "The order cannot be cancelled",
"status": 409,
"detail": "Order ord_123 has already been shipped.",
"instance": "/orders/ord_123",
"traceId": "00-abcd..."
}
Extensions can provide:
- Stable error codes.
- Field validation errors.
- Retry guidance.
- Correlation IDs.
Do not expose stack traces, SQL, credentials, or internal topology.
Resource Relationships
Represent relationships with:
- Links in representations.
- Related collection resources.
- Stable identifiers.
- Embedded summaries where useful.
{
"id": "ord_123",
"customer": {
"id": "cus_42",
"href": "/customers/cus_42"
},
"shipmentsHref": "/orders/ord_123/shipments"
}
Avoid copying entire mutable resources into every response unless the snapshot has business meaning.
Hypermedia
Hypermedia exposes available links and actions based on current state:
{
"id": "ord_123",
"status": "pendingPayment",
"links": [
{
"rel": "self",
"href": "/orders/ord_123",
"method": "GET"
},
{
"rel": "payment",
"href": "/orders/ord_123/payments",
"method": "POST"
}
]
}
Benefits:
- Clients discover related resources.
- State-dependent actions are explicit.
- URI construction logic is reduced.
Costs:
- More contract design.
- Client tooling may not use it.
- Link semantics must be documented.
Full hypermedia is not mandatory for every practical HTTP API, but links are useful for pagination, long-running operations, and discoverability.
Caching
GET and HEAD can use HTTP caching:
HTTP/1.1 200 OK
Cache-Control: private, max-age=60
ETag: "order-123-v7"
Vary: Accept-Encoding
Conditional retrieval:
GET /orders/ord_123 HTTP/1.1
If-None-Match: "order-123-v7"
HTTP/1.1 304 Not Modified
ETag: "order-123-v7"
Cache policy must consider:
- User-specific data.
- Authorization.
- Staleness tolerance.
- Intermediary caches.
- Varying representations.
- Invalidation.
Optimistic Concurrency
Use entity tags with preconditions to avoid lost updates:
GET /orders/ord_123 HTTP/1.1
HTTP/1.1 200 OK
ETag: "v7"
PATCH /orders/ord_123 HTTP/1.1
If-Match: "v7"
Content-Type: application/merge-patch+json
{
"shippingAddress": {
"city": "Da Nang"
}
}
If the resource changed:
HTTP/1.1 412 Precondition Failed
409 Conflict describes a semantic state conflict. 412 specifically indicates a failed HTTP precondition.
Long-Running Operations
Do not hold an HTTP request open for long processing when a durable asynchronous workflow is more appropriate.
POST /reports HTTP/1.1
Content-Type: application/json
{
"type": "annualRevenue",
"year": 2025
}
HTTP/1.1 202 Accepted
Location: /operations/op_789
Retry-After: 5
GET /operations/op_789 HTTP/1.1
{
"id": "op_789",
"status": "running",
"result": null
}
The operation resource should expose:
- Current status.
- Progress if meaningful.
- Failure details.
- Result link.
- Cancellation when supported.
- Retention policy.
Resource-Oriented State Transitions
Some actions can be represented as subordinate resources:
POST /orders/ord_123/cancellations
This creates a cancellation request or record with its own identity and lifecycle.
POST /orders/ord_123/payments
This creates a payment attempt rather than pretending to update a payment flag.
This approach is useful when the action:
- Has a result or status.
- Can fail independently.
- Needs audit history.
- Can be retried or reversed.
- Has its own lifecycle.
When RPC-Style Endpoints Are Acceptable
RPC-style endpoints name an operation:
POST /orders/ord_123:cancel
POST /payments/pay_42:capture
POST /documents/doc_7:sign
POST /routes:calculate
They are acceptable when:
- The operation is a meaningful business command.
- It does not map honestly to CRUD.
- Inventing a noun would be artificial.
- The command's intent matters more than representation replacement.
- The operation has complex input or validation.
- The API is primarily command-oriented.
An action endpoint should still define:
- Whether it is safe or idempotent.
- Retry behavior.
- Preconditions.
- Status codes.
- Error contracts.
- Result or operation resources.
REST and RPC are not moral categories. A consistent HTTP API can combine resource-oriented reads with explicit commands.
Prefer a Resource When the Result Has a Lifecycle
Before creating an action endpoint, ask whether the action produces a resource.
Instead of:
POST /orders/123/start-refund
consider:
POST /orders/123/refunds
A refund:
- Has an identity.
- Has status.
- Can be retrieved.
- Can fail.
- May have multiple attempts.
- Needs audit history.
Resource modeling becomes clearer when the operation has durable state.
RPC for Calculations and Queries
A pure calculation can use GET when it is safe and parameters fit a URI:
GET /shipping-quotes?origin=SGN&destination=HAN&weight=10
Use POST when:
- Input is large or structured.
- Sensitive inputs should not appear in URLs and logs.
- The calculation request has complex content.
- A quote resource is created.
POST /shipping-quotes
Content-Type: application/json
{
"origin": { "postalCode": "700000" },
"destination": { "postalCode": "100000" },
"packages": [
{
"weightKg": 10
}
]
}
Do not use GET with a request body. Its semantics and interoperability are poorly supported.
Bulk Operations
Bulk operations reduce network overhead but complicate atomicity and error reporting.
POST /orders/batch
Define:
- Maximum batch size.
- Whether processing is atomic.
- Per-item status.
- Ordering.
- Idempotency.
- Partial failure behavior.
- Asynchronous processing thresholds.
Example result:
{
"results": [
{
"clientReference": "a1",
"status": 201,
"location": "/orders/ord_1"
},
{
"clientReference": "a2",
"status": 422,
"errorCode": "invalid-product"
}
]
}
Avoid returning one vague status when clients need to reconcile individual items.
API Models Versus Domain Models
The API representation is an external contract. The domain model enforces internal business rules.
They differ because:
- API contracts require compatibility.
- Domain models evolve with business understanding.
- Authorization can hide fields.
- Responses may combine several read sources.
- API input should express client intent.
- Domain entities contain behavior not meant for serialization.
Map explicitly:
public sealed record CancelOrderRequest(
string Reason,
string? Comment);
public sealed record OrderResponse(
string Id,
string Status,
MoneyResponse Total);
Do not bind request JSON directly onto tracked domain entities.
ASP.NET Core Example
app.MapPost(
"/orders/{orderId}:cancel",
async (
string orderId,
CancelOrderRequest request,
ICancelOrderHandler handler,
CancellationToken cancellationToken) =>
{
var result = await handler.Handle(
new CancelOrderCommand(
OrderId.Parse(orderId),
request.Reason,
request.Comment),
cancellationToken);
return result.Match(
success => Results.Ok(success),
notFound => Results.NotFound(),
conflict => Results.Conflict(
new ProblemDetails
{
Title = "The order cannot be cancelled",
Detail = conflict.Message,
Status = StatusCodes.Status409Conflict
}));
});
The route is command-oriented, but the implementation still respects HTTP status and content semantics.
Security Considerations
Resource design affects security:
- Authorize every item, not only the collection route.
- Do not trust resource IDs to imply ownership.
- Prevent mass assignment.
- Limit fields clients can filter and sort.
- Bound request and response sizes.
- Avoid leaking resource existence where policy requires concealment.
- Validate content types.
- Use rate limits for expensive operations.
- Avoid putting secrets in paths or query strings.
404 can intentionally hide whether a resource exists. This should be a consistent policy, not accidental behavior.
Common Mistakes
- Mirroring database tables as resources.
- Using verbs in every URI.
- Treating REST as a route-naming convention only.
- Mutating state through
GET. - Using
POSTfor every operation without defining semantics. - Implementing partial merge behavior under
PUT. - Assuming every
PATCHis idempotent. - Returning
200for errors. - Returning ORM entities directly.
- Ignoring
Locationafter creation. - Using
202without a status resource. - Nesting paths too deeply.
- Inventing awkward resources to avoid all action endpoints.
- Using RPC actions without retry and concurrency rules.
- Confusing soft deletion, cancellation, and deactivation.
- Ignoring caching and conditional requests.
Best Practices
- Model stable business concepts and workflows as resources.
- Separate resources from their representations and persistence models.
- Use consistent collection and item URIs.
- Apply HTTP method semantics honestly.
- Use
201andLocationfor newly created resources. - Use conditional requests for caching and concurrency.
- Standardize errors with Problem Details.
- Represent long-running work with operation resources.
- Prefer subordinate resources for actions with identity and lifecycle.
- Use RPC-style endpoints for genuine commands that do not fit CRUD.
- Document idempotency, retry, authorization, and failure behavior for every operation.
- Keep API contracts independent from domain and ORM classes.