REST API Design Best Practices: Building APIs That Scale
Why REST API Design Matters
A good REST API feels obvious. Developers can guess the next endpoint, understand errors immediately, and rely on stable behavior. Poor API design creates hidden costs: more support, slower delivery, and brittle integrations.
This guide focuses on practical decisions that improve real-world APIs: resource structure, consistent contracts, error design, pagination, security, and performance.
Core Principles
Consistency is the foundation. Every endpoint should behave the same way, even as features evolve.
- Consistency beats cleverness. Predictable APIs are faster to use and safer to change.
- Clarity over brevity. Choose obvious names over short names.
- Design for change. Plan for versioning and backward compatibility.
- Make failures actionable. Errors should tell users what to fix.
Resource Design
1) Use Nouns, Not Verbs
URLs should describe resources, not actions.
Bad
GET /getUsers
POST /createNewUser
DELETE /deleteUserById/123
Good
GET /users
POST /users
GET /users/123
DELETE /users/123
2) Keep Paths Predictable
Short, flat paths are easier to understand and evolve.
GET /users/123/orders
GET /orders/456
Avoid deep nesting:
// Avoid
/users/123/orders/456/items/789/reviews
// Prefer
/order-items/789/reviews
HTTP Methods and Idempotency
HTTP methods communicate intent. That only works if your API uses them consistently.
- GET: safe and idempotent
- POST: create new resource, not idempotent
- PUT: replace resource, idempotent
- PATCH: partial update, idempotent if designed correctly
- DELETE: remove resource, idempotent
When retries are possible (payments, emails), use idempotency keys to prevent duplicates.
POST /payments
Idempotency-Key: 9f1b8c9f-9a4e-4c3a-9e5c-0f8f2cbb7a12
Status Codes and Error Design
Use a small, consistent set of status codes. Then make errors predictable.
- 200 OK
- 201 Created
- 204 No Content
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
- 409 Conflict
- 422 Validation errors
- 429 Rate limit
- 500 Server error
Error Response Format
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid data",
"details": [
{ "field": "email", "message": "Email format is invalid" }
],
"requestId": "req-123456",
"path": "/users"
}
}
The structure never changes. Clients can safely parse it, and developers know where to look.
Pagination, Filtering, and Sorting
Pagination should be available on every list endpoint, even if the dataset is small today.
Cursor-based (recommended for large datasets)
GET /users?cursor=eyJpZCI6MTIzfQ&limit=20
Offset-based (fine for small datasets)
GET /users?page=2&limit=20
Filtering and sorting should follow a single, consistent syntax.
GET /users?role=admin&status=active
GET /orders?createdAfter=2024-01-01&createdBefore=2024-12-31
GET /users?sort=lastName:asc,createdAt:desc
GET /users?fields=id,firstName,lastName,email
Versioning
Version only when you must, but plan for it from day one. The worst time to introduce versioning is after you break clients.
Path versioning
GET /v1/users/123
GET /v2/users/123
Header versioning
GET /users/123
Accept: application/vnd.myapi.v2+json
Security Essentials
Security should be a design requirement, not a later addition.
Authentication and Authorization
- Use OAuth2/JWT for user authentication.
- Use API keys for service-to-service access.
- Enforce role-based access at the resource level.
Rate Limiting
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1640000000
Input Validation
Validate both format and business rules. Return 422 for validation errors.
Performance and Reliability
Caching
Cache-Control: public, max-age=300
ETag: "33a64df551425fcc"
Bulk Operations
Provide batch endpoints to reduce round trips when appropriate.
POST /users/bulk
PATCH /users/bulk
DELETE /users/bulk
Long-Running Jobs
If a task takes long, return 202 Accepted and provide a status endpoint.
POST /reports
202 Accepted
Location: /jobs/789
Consistency Checklist
- Field names use lowerCamelCase (or snake_case, but never both).
- Dates are ISO-8601 strings.
- Booleans are true booleans, not “yes/no”.
- Arrays are always arrays, even when empty.
- Response envelope is the same for every endpoint.
Real-World Example (User API)
GET /v1/users?role=admin&status=active&page=1&limit=20&sort=lastName:asc
Response:
{
"success": true,
"data": [
{
"id": 123,
"email": "john.admin@example.com",
"firstName": "John",
"lastName": "Admin",
"role": "admin",
"status": "active",
"createdAt": "2024-01-15T10:30:00Z"
}
],
"meta": {
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"totalPages": 1,
"hasNext": false,
"hasPrevious": false
}
}
}
Common Pitfalls
- Inconsistent naming across endpoints.
- GET used for mutations.
- Unhelpful errors with no actionable details.
- No versioning for breaking changes.
- Exposing internal IDs or DB schema.
- No rate limit on public endpoints.
Key Takeaways
Good REST APIs are predictable. They do not surprise clients, and they guide users toward correct usage.
- Design URLs around resources.
- Use HTTP methods consistently.
- Provide structured, actionable errors.
- Paginate every list endpoint.
- Secure and rate-limit from day one.
- Keep contracts stable and version only when needed.