REST API design is easy to start and hard to master. This guide covers the decisions that matter in production: resources, HTTP semantics, pagination, filtering, errors, authentication, versioning, idempotency, and documentation.
If you're building a backend for a web or mobile app, chances are you need a RESTful API. A well-designed REST API improves developer experience, makes frontends faster to build, reduces bugs, and prevents breaking changes later.
What “proper” REST API design means
In REST API design, your URLs should represent nouns (resources), not verbs (actions). The behavior comes from the HTTP method.
# Good (resources)
GET /users
GET /users/{userId}
POST /users
PATCH /users/{userId}
DELETE /users/{userId}
# Avoid (verbs/actions)
POST /createUser
POST /updateUser
POST /deleteUser
When your API reads like a clean data model, it becomes easier to extend. For example, a user’s posts can become:
GET /users/{userId}/posts
POST /users/{userId}/posts
A simple rule: if you can replay a request safely, design it to be idempotent (more on that later).
Status codes are part of your API contract. Keep them consistent:
Success
200 OK (read/update)201 Created (new resource)204 No Content (delete)Client/Server Errors
400 Bad Request (validation)401 Unauthorized (no auth)403 Forbidden (no permission)404 Not Found409 Conflict429 Too Many Requests500 Internal Error{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email is invalid",
"details": [
{ "field": "email", "reason": "Must be a valid email address" }
],
"requestId": "req_9f8d2a..."
}
}
Include a requestId so you can trace issues quickly in logs. This is a senior-level API design move that pays off in production.
Lists are where APIs often become inconsistent. Pick one approach and use it everywhere.
GET /users?limit=20&offset=40
GET /users?limit=20&cursor=eyJpZCI6IjEwMDAifQ==
Cursor pagination avoids duplicates and missing items when data changes rapidly. If you’re building for scale, cursor-based pagination is usually the right long-term choice.
GET /orders?status=paid&sort=-createdAt
Authentication answers: “Who are you?” Authorization answers: “What can you do?”
Keep authorization in a centralized layer (middleware/policies), not scattered across endpoints. That’s how you prevent privilege bugs.
Input validation is a security feature and a developer experience feature. Validate at the edge (request layer) and return structured errors.
Also: define an API contract using OpenAPI (Swagger). Contracts make it easier to generate clients, test, and document.
Versioning is not optional if your API will be used by more than one client. Common approaches:
/v1/users (simple, explicit)Accept: application/vnd.myapi.v1+json (clean URLs)Prefer backward-compatible changes whenever possible: add fields (never remove), add endpoints, and keep default behavior stable.
In real systems, clients retry. Networks fail. Timeouts happen. If a request can accidentally double-charge or double-create a record, you have a production incident waiting to happen.
Add an Idempotency-Key header for safe POST operations that create payments/orders:
POST /payments
Idempotency-Key: 7b1d2b8e-5c6c-4f9f-9b13-...
{ "amount": 5000, "currency": "LKR", "orderId": "ord_123" }
Store the key and response for a fixed time window. If the same key is seen again, return the same result.
Public and mobile APIs should include rate limiting:
Use 429 with clear headers:
Retry-After, and optionally metadata like remaining quota.
A REST API that’s hard to use is a slow product. Document:
Production REST API checklist
Designing REST APIs properly is a key skill for any backend or full stack engineer. The best APIs feel boring: predictable, consistent, safe to evolve, and easy to integrate.
I’m Sandaruwan Jayasundara — Senior Software Engineer | Full Stack Developer. I write practical guides on scalable architecture, backend engineering, and cloud delivery. Explore more at sandaruwan.dev.