A well-designed REST API feels predictable: the same URL patterns, status codes, and error shapes appear everywhere, so integrators spend time on business logic instead of guessing your conventions. This guide walks through resource modeling, HTTP semantics, pagination, versioning, and documentation patterns you can apply on your next endpoint.
What REST actually means in practice
REST (Representational State Transfer) is not a framework — it is a set of constraints that make APIs cacheable, scalable, and easy to reason about. In day-to-day engineering, that translates to:
- Resources identified by URLs (
/invoices/42, not/getInvoice?id=42) - Representations exchanged as JSON (or XML, but JSON dominates)
- Stateless requests where the server does not rely on session memory between calls
- Uniform interface using standard HTTP methods and status codes
You do not need HATEOAS hypermedia links on every response to call your API RESTful. What matters is consistency: if GET /users/7 returns a user object today, it should not suddenly require a POST tomorrow.
Naming resources and URLs
Treat URLs as nouns, not verbs. Collections are plural; individual items append an identifier.
GET /v1/projects # list projects
POST /v1/projects # create project
GET /v1/projects/{id} # read one
PATCH /v1/projects/{id} # partial update
DELETE /v1/projects/{id} # removeNested resources work when the child cannot exist without a clear parent context:
GET /v1/projects/{projectId}/tasks
POST /v1/projects/{projectId}/tasksAvoid deep nesting beyond two levels (/a/{id}/b/{id}/c/{id}). Prefer flat URLs with query filters:
GET /v1/tasks?project_id=abc123&status=openUse kebab-case in path segments (/payment-methods) and lowercase throughout. Query parameters use snake_case or camelCase — pick one and document it.
Actions that are not CRUD
When an operation does not map cleanly to PUT or PATCH, use a sub-resource with a verb-like noun:
POST /v1/invoices/{id}/send
POST /v1/accounts/{id}/password-resetReserve RPC-style paths for exceptional cases. If more than 20% of your endpoints are actions, reconsider your resource model.
HTTP methods and idempotency
| Method | Safe | Idempotent | Typical use |
|---|---|---|---|
| GET | Yes | Yes | Read |
| POST | No | No | Create |
| PUT | No | Yes | Replace entire resource |
| PATCH | No | No* | Partial update |
| DELETE | No | Yes | Remove |
*PATCH idempotency depends on implementation; design PATCH payloads so repeating the same request yields the same final state.
POST creates resources and returns 201 Created with a Location header pointing to the new URL:
POST /v1/customers HTTP/1.1
Content-Type: application/json
{"email": "ada@example.com", "plan": "pro"}HTTP/1.1 201 Created
Location: /v1/customers/cust_8f2a
Content-Type: application/json
{"id": "cust_8f2a", "email": "ada@example.com", "plan": "pro", "created_at": "2026-07-16T08:00:00Z"}Use PUT only when the client sends the full representation. Partial updates belong on PATCH with a documented merge strategy (JSON Merge Patch RFC 7396 or JSON Patch RFC 6902).
Status codes that communicate intent
Clients and proxies behave differently based on status families. Use them precisely:
2xx success
200 OK— successful GET, PATCH, or action201 Created— successful POST that created a resource204 No Content— successful DELETE or update with no body
4xx client errors
400 Bad Request— malformed JSON or failed validation401 Unauthorized— missing or invalid authentication (despite the name, this means "unauthenticated")403 Forbidden— authenticated but not permitted404 Not Found— resource does not exist (or is hidden for authorization)409 Conflict— duplicate email, optimistic locking failure422 Unprocessable Entity— syntactically valid JSON that fails business rules429 Too Many Requests— rate limited; includeRetry-After
5xx server errors
500 Internal Server Error— unexpected failure; log details server-side only503 Service Unavailable— maintenance or overload; use withRetry-After
Never return 200 OK with {"success": false} in the body. That breaks HTTP caching, monitoring, and client libraries.
Error response shape
Standardize errors so integrators can build one handler:
{
"error": {
"code": "validation_failed",
"message": "Email is already registered",
"details": [
{"field": "email", "issue": "duplicate"}
],
"request_id": "req_9xk2m"
}
}Include a stable code string (snake_case) for programmatic branching. Human-readable message text can change; code should not without a version bump. Always echo a request_id from your tracing system.
Paste sample responses into the /tools/json-formatter during design reviews to catch trailing commas and schema drift before they reach production.
Pagination, filtering, and sorting
Offset pagination is simple but degrades on large tables:
GET /v1/events?limit=50&offset=100Cursor pagination scales better for feeds and audit logs:
GET /v1/events?limit=50&cursor=eyJpZCI6MTAwMH0Response envelope:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTA1MH0",
"has_more": true
}
}Document filter operators explicitly. Prefer status=active,inactive over inventing a mini query language unless you have full-text search requirements.
Versioning strategies
Pick one approach and apply it everywhere:
- URL prefix —
/v1/...(most visible, easiest for logs and routing) - Header —
Accept: application/vnd.myapp.v2+json(clean URLs, harder to test in browser) - Query param —
/users?api_version=2(generally avoid; caches treat query strings inconsistently)
When breaking changes are unavoidable, ship /v2 alongside /v1 with a published sunset date. Non-breaking additions (new optional fields) do not require a version bump if clients ignore unknown keys.
Authentication and authorization headers
Most APIs use bearer tokens:
GET /v1/me HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...Document which endpoints accept API keys vs OAuth tokens. Never accept credentials in query strings — they end up in access logs and browser history.
For machine-to-machine calls, separate scopes from user permissions in documentation. A token with read:invoices should not silently grant write:invoices.
Content negotiation and headers
Default to Content-Type: application/json; charset=utf-8. Support If-None-Match and ETag on cacheable GET responses to save bandwidth:
HTTP/1.1 200 OK
ETag: "a1b2c3"
Cache-Control: private, max-age=60Use Idempotency-Key on POST endpoints that create billable or side-effectful resources (payments, shipments):
POST /v1/charges HTTP/1.1
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000The server stores the key for 24 hours and returns the same response if the client retries.
Designing for integrators
Ship an OpenAPI 3 document generated from code or maintained as source of truth. Include:
- Example requests and responses for every endpoint
- Rate limit headers (
X-RateLimit-Remaining,X-RateLimit-Reset) - Webhook payload schemas if you push events
- Sandbox base URL with test API keys
Test flows manually with /tools/rest-client and /tools/api-request-tester before publishing docs — broken examples erode trust faster than missing features.
REST API design checklist
Before merging a new endpoint:
- URL uses plural nouns and matches existing patterns
- Correct HTTP method and idempotency documented
- Status codes match outcome (no success flags in 200 bodies)
- Error envelope includes
code,message, andrequest_id - Pagination uses cursors for high-volume collections
- Breaking changes gated behind a new API version
- OpenAPI spec updated with runnable examples
- Auth requirements stated per endpoint
Bulk and batch operations
When clients create or update many resources, avoid forcing N sequential POSTs. Offer a batch endpoint with partial success semantics:
POST /v1/products/batch HTTP/1.1
Content-Type: application/json
{
"operations": [
{"method": "POST", "body": {"sku": "A1", "price_cents": 999}},
{"method": "PATCH", "path": "/v1/products/prod_7", "body": {"price_cents": 1299}}
]
}Return 207 Multi-Status or 200 with per-item results:
{
"results": [
{"status": 201, "id": "prod_9", "location": "/v1/products/prod_9"},
{"status": 404, "error": {"code": "not_found", "message": "prod_7 missing"}}
]
}Document maximum batch size (typically 25–100 items) and enforce it server-side to protect database connection pools.
Common mistakes to avoid
Leaking database internals. Exposing auto-increment integer IDs enables enumeration attacks. Prefer opaque IDs (usr_, UUIDs, or ULIDs).
Over-fetching by default. List endpoints should return summary objects; offer ?expand=line_items for detail when needed.
Inconsistent timestamps. Always store UTC, always serialize ISO-8601 with Z suffix: 2026-07-16T14:30:00Z.
Ignoring OPTIONS and CORS. Browser clients need explicit Access-Control-Allow-Methods and preflight handling on APIs meant for frontends.
Undocumented null vs omitted fields. State whether missing keys and explicit null mean the same thing on PATCH.
Conclusion
Great REST APIs optimize for the integrator's mental model: predictable URLs, honest status codes, and errors that explain what went wrong. Start with a small resource map, enforce conventions in code review, and publish examples you have actually executed.
Next steps: Draft an OpenAPI spec for one service, run every example through /tools/rest-client, and read the GraphQL vs REST comparison if you are deciding whether REST fits your client needs. For rate limiting patterns, see the API rate limiting guide.
Try these free tools
Put what you learned into practice — no signup required.