How to Design REST APIs Properly
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
- Resources are modeled cleanly and consistently
- HTTP methods and status codes have predictable meaning
- Errors are structured and actionable
- Pagination/filtering are standardized across endpoints
- Auth, rate limiting, validation, and security are built-in
- APIs evolve without breaking clients (versioning strategy)
1) Model your API around resources (not actions)
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
2) Use HTTP methods correctly
- GET fetches data (no side effects)
- POST creates something or triggers server-side processing
- PUT replaces a resource (full update)
- PATCH partially updates a resource
- DELETE removes a resource (or marks it deleted)
A simple rule: if you can replay a request safely, design it to be idempotent (more on that later).
3) Standardize status codes and responses
Status codes are part of your API contract. Keep them consistent:
Success
200OK (read/update)201Created (new resource)204No Content (delete)
Client/Server Errors
400Bad Request (validation)401Unauthorized (no auth)403Forbidden (no permission)404Not Found409Conflict429Too Many Requests500Internal Error
A good error format (actionable + consistent)
{
"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.
4) Pagination, filtering, sorting: make it universal
Lists are where APIs often become inconsistent. Pick one approach and use it everywhere.
Offset pagination (simple)
GET /users?limit=20&offset=40
Cursor pagination (better for large datasets)
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.
Filtering + sorting pattern
GET /orders?status=paid&sort=-createdAt
5) Authentication and authorization (don’t mix them)
Authentication answers: “Who are you?” Authorization answers: “What can you do?”
- Use secure cookie sessions for browser apps (often best)
- Use OAuth2 / JWT for external integrations and mobile apps
- Implement role-based access control (RBAC) or policy-based rules
Keep authorization in a centralized layer (middleware/policies), not scattered across endpoints. That’s how you prevent privilege bugs.
6) Validation and contracts: treat your API like a product
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.
7) Versioning strategy: evolve without breaking clients
Versioning is not optional if your API will be used by more than one client. Common approaches:
- URL versioning:
/v1/users(simple, explicit) - Header versioning:
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.
8) Idempotency: make retries safe
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.
9) Rate limiting and abuse prevention
Public and mobile APIs should include rate limiting:
- Per-IP limits for anonymous traffic
- Per-user/app limits for authenticated traffic
- Stricter limits for expensive endpoints (search, exports)
Use 429 with clear headers:
Retry-After, and optionally metadata like remaining quota.
10) Documentation: make your API easy to adopt
A REST API that’s hard to use is a slow product. Document:
- Authentication (how to get tokens/sessions)
- Common error codes and how to fix them
- Examples for requests and responses
- Pagination and filtering rules
Production REST API checklist
- Consistent resource naming
- Correct HTTP semantics
- Structured errors + requestId
- Standard pagination + sorting
- Auth + authorization policies
- Validation at the boundary
- Versioning strategy
- Idempotency for critical operations
- Rate limiting and monitoring
- OpenAPI documentation
Final thoughts
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.