REST API Design Best Practices: Build Developer-Friendly APIs
Design REST APIs developers love using resource-based URLs, consistent error handling, versioning, pagination, and OpenAPI documentation for scalable integrations.

Design REST APIs developers love using resource-based URLs, consistent error handling, versioning, pagination, and OpenAPI documentation for scalable integrations.

TL;DR
/users/123, not /getUser?id=123); HTTP verbs express actions.error.code, error.message, error.details./v1/users) or header (Accept: application/vnd.api+json; version=1).Jump to URL design · Jump to Request/response format · Jump to Error handling · Jump to Versioning
Your API is your product's interface to the world -poor design creates friction, support burden, and limits adoption. These REST API design best practices create intuitive, scalable APIs developers actually enjoy using.
Key takeaways
- Resource-based URLs + HTTP verbs = self-documenting API surface.
- Consistent error responses (codes, messages, details) reduce integration time 50%.
- OpenAPI documentation enables auto-generated SDKs and interactive testing.
Pattern: /resources/{id}/subresources/{id}
Good URLs:
GET /users # List users
GET /users/123 # Get user 123
POST /users # Create user
PUT /users/123 # Update user 123
DELETE /users/123 # Delete user 123
GET /users/123/posts # Get posts by user 123
Bad URLs:
GET /getUser?id=123 # Verb in URL (use GET /users/123)
POST /users/delete # DELETE method exists
GET /user-posts?userId=123 # Use /users/123/posts
Recommendation: Always plural (/users, /posts), even for singleton resources.
Why: Consistency. Exception: Singleton resources like /me (current user), /status (health check).
GET /users?status=active&sort=created_at:desc&limit=50&offset=100
Standard params:
?status=active&role=admin?sort=created_at:desc,name:asc?limit=50&offset=100 or ?cursor=abc123?fields=id,name,email (reduce payload)"The developer experience improvements we've seen from AI tools are the most significant since IDEs and version control. This is a permanent shift in how software gets built." - Emily Freeman, VP of Developer Relations at AWS
Success response (200 OK):
{
"data": {
"id": "usr_123",
"name": "Alice Chen",
"email": "alice@example.com",
"created_at": "2025-04-25T10:30:00Z"
}
}
Collection response (200 OK):
{
"data": [
{ "id": "usr_123", "name": "Alice Chen" },
{ "id": "usr_456", "name": "Bob Smith" }
],
"meta": {
"total": 1247,
"limit": 50,
"offset": 100
},
"links": {
"next": "/users?limit=50&offset=150",
"prev": "/users?limit=50&offset=50"
}
}
Why wrap in data: Allows adding meta, links, included fields without breaking clients.
| Code | Meaning | Use case |
|---|---|---|
| 200 OK | Success | GET, PUT, PATCH succeeded |
| 201 Created | Resource created | POST succeeded |
| 204 No Content | Success, no body | DELETE succeeded |
| 400 Bad Request | Client error | Validation failed, malformed JSON |
| 401 Unauthorized | Auth failed | Missing/invalid API key |
| 403 Forbidden | Insufficient permissions | User lacks access to resource |
| 404 Not Found | Resource doesn't exist | GET /users/999 (no user 999) |
| 429 Too Many Requests | Rate limit exceeded | Client hit 100 req/min limit |
| 500 Internal Server Error | Server error | Unhandled exception |
Rule: 2xx = success, 4xx = client error, 5xx = server error.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email address is invalid",
"details": [
{
"field": "email",
"issue": "Must be valid email format"
}
],
"request_id": "req_abc123"
}
}
Fields:
code: Machine-readable (e.g., RATE_LIMIT_EXCEEDED, RESOURCE_NOT_FOUND).message: Human-readable description.details: Field-level validation errors (for 400 responses).request_id: Trace logs for debugging.| Code | HTTP Status | Example message |
|---|---|---|
INVALID_REQUEST | 400 | "Missing required field: name" |
UNAUTHORIZED | 401 | "API key invalid or expired" |
FORBIDDEN | 403 | "Insufficient permissions to delete user" |
NOT_FOUND | 404 | "User usr_123 not found" |
RATE_LIMIT_EXCEEDED | 429 | "Rate limit: 100 requests/minute exceeded" |
INTERNAL_ERROR | 500 | "An unexpected error occurred" |
Example (Stripe-style):
{
"error": {
"type": "invalid_request_error",
"code": "parameter_invalid_integer",
"message": "Invalid integer: abc",
"param": "limit"
}
}
For integration best practices, see /blog/zapier-vs-make-vs-n8n-ai-ops.
GET /v1/users/123
GET /v2/users/123 # Breaking change: response schema differs
Pros: Explicit, easy to route, clear deprecation path.
Cons: Version in every URL; harder to evolve incrementally.
When to use: Major versions (v1, v2); breaking changes (field removed, renamed).
GET /users/123
Accept: application/vnd.myapi.v1+json
Pros: Clean URLs, version decoupled from resource.
Cons: Less discoverable; clients must set headers.
When to use: SaaS platforms with long-lived API contracts.
Strategy: Never break compatibility; only add fields, endpoints, query params.
Example:
phone_number (clients ignore unknown fields).email → email_address (breaking).When to use: Internal APIs, rapid iteration pre-GA.
Recommendation: Start with URL versioning (/v1); transition to header-based for mature APIs.
Offset-based:
GET /users?limit=50&offset=100
Cursor-based:
GET /users?limit=50&cursor=usr_123_encoded
Recommendation: Offset for small datasets (<10K records), cursor for large or real-time data.
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1714041600
When limit exceeded:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit: 100 requests/minute exceeded. Retry after 60 seconds."
}
}
Tiers:
Why OpenAPI (Swagger):
Example OpenAPI spec:
openapi: 3.0.0
info:
title: Users API
version: 1.0.0
paths:
/v1/users:
get:
summary: List users
parameters:
- name: limit
in: query
schema:
type: integer
default: 50
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: string
name:
type: string
email:
type: string
Tools: Stoplight Studio (visual editor), Redocly (docs hosting), Postman (import OpenAPI).
Call-to-action (API design) Audit existing API endpoints against these principles; refactor top 3 inconsistencies before adding new endpoints.
REST: Simpler, cacheable, better for public APIs, CRUD operations.
GraphQL: Flexible queries, reduces over-fetching, better for complex UIs with varied data needs.
Recommendation: Start with REST; add GraphQL if frontend has 10+ bespoke queries.
HATEOAS example:
{
"data": {
"id": "usr_123",
"name": "Alice"
},
"links": {
"self": "/users/123",
"posts": "/users/123/posts"
}
}
Pros: Discoverability, decouples clients from URL construction.
Cons: Verbose, rarely implemented fully.
Recommendation: Include links for pagination (next, prev); skip for every resource (overkill).
Multipart form-data:
POST /users/123/avatar
Content-Type: multipart/form-data
--boundary
Content-Disposition: form-data; name="file"; filename="avatar.jpg"
...
Alternative: Direct upload to S3, return URL to API:
/uploads/presigned-url → {url, fields}/users/123/avatar with {url: "s3://..."}Design: POST to customer-configured URL with event payload.
Best practices:
event.type (user.created, payment.succeeded).Design REST APIs with resource-based URLs, consistent JSON responses, structured errors, versioning, and OpenAPI documentation for excellent developer experience.
Next steps
Internal links
External references
Crosslinks