How to Design a REST API That Developers Love
How to Design a REST API That Developers Love
The best APIs share a quality: developers figure them out without reading the docs. Resource naming is intuitive. Error messages explain what went wrong and how to fix it. Pagination works consistently. Authentication is a single header. Here's how to design APIs that developers reach for.
1. Resource Naming
Use Nouns, Not Verbs
Resources are things, not actions. HTTP methods provide the verbs.
✅ GET /users → List users
✅ POST /users → Create user
✅ GET /users/123 → Get user
✅ PUT /users/123 → Update user
✅ DELETE /users/123 → Delete user
❌ GET /getUsers
❌ POST /createUser
❌ POST /deleteUser/123
Use Plural Nouns
Consistency matters more than grammar debates. Use plurals everywhere.
✅ /users
✅ /orders
✅ /products
❌ /user
❌ /order/123 (mixing singular and plural)
Nest for Relationships
Sub-resources express relationships. Keep nesting to 2 levels max.
✅ GET /users/123/orders → User's orders
✅ GET /users/123/orders/456 → Specific order
❌ GET /users/123/orders/456/items/789/reviews → Too deep
Use kebab-case
✅ /api/order-items
✅ /api/user-profiles
❌ /api/orderItems
❌ /api/order_items
2. HTTP Methods and Status Codes
Methods
| Method | Purpose | Idempotent | Safe |
|---|---|---|---|
| GET | Read resource(s) | Yes | Yes |
| POST | Create resource | No | No |
| PUT | Replace resource (full) | Yes | No |
| PATCH | Update resource (partial) | No | No |
| DELETE | Remove resource | Yes | No |
Status Codes
Use specific codes. Don't return 200 for everything.
| Code | Meaning | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH, DELETE |
| 201 | Created | Successful POST (include Location header) |
| 204 | No Content | Successful DELETE with no response body |
| 400 | Bad Request | Invalid input (validation errors) |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Duplicate resource, state conflict |
| 422 | Unprocessable Entity | Semantically invalid input |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected server failure |
3. Error Handling
The Error Response
Every error should include a machine-readable code, human-readable message, and actionable detail.
{
"error": {
"code": "validation_error",
"message": "The request body contains invalid fields.",
"details": [
{
"field": "email",
"code": "invalid_format",
"message": "Must be a valid email address."
},
{
"field": "age",
"code": "out_of_range",
"message": "Must be between 13 and 120."
}
],
"request_id": "req_abc123",
"documentation_url": "https://api.example.com/docs/errors#validation_error"
}
}
Error Design Rules
- Machine-readable codes —
validation_error,not_found,rate_limit_exceeded - Human-readable messages — explain what happened in plain English
- Field-level details — for validation errors, tell the client which fields failed and why
- Request ID — for debugging and support tickets
- Documentation link — point to docs explaining the error
4. Pagination
Cursor-Based (Recommended)
{
"data": [...],
"pagination": {
"has_more": true,
"next_cursor": "eyJpZCI6MTIzfQ=="
}
}
Request: GET /users?limit=20&cursor=eyJpZCI6MTIzfQ==
Pros: Consistent results when data changes. Performs well at any depth. Cons: Can't jump to page 5 directly. Can't show "page 3 of 10."
Offset-Based
{
"data": [...],
"pagination": {
"total": 500,
"limit": 20,
"offset": 40,
"has_more": true
}
}
Request: GET /users?limit=20&offset=40
Pros: Simple. Can jump to any page. Can show total count. Cons: Inconsistent results if data changes between pages. Slow at deep offsets (OFFSET 10000).
Recommendation: Cursor-based for most APIs. Offset-based only when random access is required.
5. Filtering and Sorting
GET /users?status=active&role=admin → Filter
GET /users?sort=created_at&order=desc → Sort
GET /users?fields=id,name,email → Sparse fields
GET /users?search=john → Search
Keep It Consistent
- Use the same parameter names across all endpoints
sortfor field name,orderfor direction (asc/desc)limitandcursor/offsetfor paginationfieldsfor sparse fieldsetssearchfor full-text search
6. Versioning
Start with URL path versioning: /api/v1/users. It's the most understood approach.
Rules:
- Add fields without incrementing version (additive changes are not breaking)
- Increment version for breaking changes (field removal, type changes, behavior changes)
- Support old versions for 12+ months after deprecation announcement
- Document what constitutes a "breaking change" for your API
7. Authentication
Use Authorization: Bearer <token> header. Don't put tokens in URLs (they leak in logs, referrers, and browser history).
✅ Authorization: Bearer sk_live_abc123
❌ GET /users?api_key=sk_live_abc123
8. Response Envelope
Consistent Wrapping
Wrap all responses in a consistent envelope:
{
"data": { ... },
"meta": {
"request_id": "req_abc123",
"timestamp": "2026-03-08T12:00:00Z"
}
}
For lists:
{
"data": [ ... ],
"pagination": { ... },
"meta": { ... }
}
9. Learn from the Best
| API | What They Do Well |
|---|---|
| Stripe | Consistent naming, excellent errors, idempotency keys, expandable objects |
| GitHub | Discoverable (HATEOAS links), consistent pagination, excellent docs |
| Twilio | Resource-oriented, consistent patterns across all products |
| Slack | Simple methods, clear error codes, rate limit transparency |
| OpenAI | Clean JSON, streaming support, clear model selection |
Designing APIs? Explore API design tools, gateways, and best practices on APIScout — comparisons, guides, and developer resources for building great APIs.