Designing a clean REST API

A clean REST API is boring in the best way: resources are easy to find, methods mean what HTTP says they mean, responses are predictable, and failures are clear enough for clients…

A clean REST API is boring in the best way: resources are easy to find, methods mean what HTTP says they mean, responses are predictable, and failures are clear enough for clients to handle without guesswork.

Start with resources, not actions

Model the API around resources that have stable identities. Use plural nouns for collections and concrete identifiers for individual resources, such as /orders and /orders/{orderId}. Keep actions out of ordinary CRUD paths. A path like /orders/{orderId}/cancel can be valid when cancellation is a domain operation with its own rules, but it should not replace normal use of HTTP methods.

Use names that match the domain language. Avoid leaking database tables, internal service names, implementation types, or workflow steps into public paths. Once a path is public, clients will build against it.

Use HTTP methods by their semantics

Use GET to read a resource or collection. GET is safe, so it must not change server state just because a client reads something. Use POST to create subordinate resources or run operations that are not idempotent. Use PUT when the client replaces a resource at a known URI. Use PATCH for partial updates when the patch format and merge rules are documented. Use DELETE to remove a resource or make it unavailable.

Idempotency matters. HTTP defines PUT, DELETE, and the safe methods as idempotent, while POST and PATCH are neither safe nor idempotent. Idempotent does not mean every repeated request returns the same status code. It means the intended effect on the server is the same after one request or many identical requests. Design with that distinction in mind.

Make status codes specific but not clever

Return status codes that match the outcome. Use 200 when a response includes a successful representation, 201 when a resource was created, 202 when work was accepted but not finished, and 204 when the operation succeeded and no body is useful. Use 400 for invalid request syntax or shape, 401 for missing or invalid authentication, 403 for authenticated clients that are not allowed to act, 404 when the resource is not available to that client, and 409 for conflicts with the current state of the resource.

Do not invent application success or failure codes inside a 200 response. Clients, proxies, SDKs, logs, tracing tools, and load balancers already understand HTTP status classes. Use them.

Standardise error bodies

Use a consistent error shape. For HTTP APIs, the problem details format is the current standard for machine-readable error responses. At minimum, include a stable error type, a short title, the HTTP status, a request-specific detail when safe to share, and an instance or correlation value that support can trace.

Do not expose stack traces, SQL fragments, provider error dumps, secrets, internal hostnames, or private identifiers. Error messages are part of the API contract. They must help the caller fix the request without exposing the internals of the system.

Keep request and response shapes consistent

Use one naming convention for JSON fields and apply it everywhere. Avoid mixing createdAt, created_at, and created across the same API. Use date and time strings with an explicit offset or UTC marker for instants, following the internet timestamp profile of ISO 8601. State whether date-only fields are calendar dates rather than timestamps.

Represent money as an object with amount and currency, or as the smallest currency unit with clear naming. Avoid floating point values for money. Represent booleans as booleans, not strings. Represent identifiers as strings unless the client is expected to perform numeric operations on them.

Design collections deliberately

Every collection that can grow should have pagination. Filtering and sorting must be documented as part of the collection contract, not added as ad hoc query parameters. Choose stable default ordering, because pagination without deterministic ordering produces duplicates and gaps when data changes.

Keep collection items useful but bounded. A list response should include the fields needed to choose or display an item. It should not include every nested object by default. Use explicit expansion or follow-up requests when large related resources are needed.

Document the contract

Keep an OpenAPI description as part of the source, review it with the code, and generate or validate examples from it where possible. Each operation should document parameters, request bodies, response bodies, status codes, authentication requirements, rate limits, and error cases.

Examples are part of the contract. Keep them small, realistic, and valid. A correct but abstract schema is not enough for a client engineer who needs to know what the API actually returns.

Version for compatibility, not convenience

A clean API changes without surprising clients. Additive changes are usually safe: new optional request fields, new response fields, new endpoints, and new enum values when clients are told to ignore unknown values. Breaking changes need a versioning and deprecation plan.

Do not publish internal refactors as API versions. A version should exist because the client contract changed, not because the server implementation changed.

Conclusion

A clean REST API is a stable contract over HTTP. Good resource names, correct method semantics, specific status codes, consistent error bodies, deliberate collection design, and accurate documentation reduce integration work for every client. The best API design is not flashy. It is predictable under success, failure, growth, and change.