API Design Best Practices
Principles and patterns for designing APIs that developers want to use and operations teams can support.
Foundational Principles
1. Design for the Consumer
APIs exist to serve their consumers. Every design decision should consider:
- Who will use this API?
- What are they trying to accomplish?
- What is their technical sophistication?
- How will they discover and learn the API?
2. Be Consistent
Consistency reduces cognitive load. Establish conventions and follow them:
- Naming patterns
- Error formats
- Pagination approach
- Authentication method
3. Be Explicit
Implicit behavior creates confusion. Make everything explicit:
- Document all behaviors
- Use clear, unambiguous names
- Provide examples for every operation
Resource Design
URL Structure
Pattern: /{version}/{resource}/{identifier}/{sub-resource}
Good:
GET /v1/users/123/orders
POST /v1/orders
PUT /v1/orders/456
DELETE /v1/orders/456
Avoid:
GET /getUser?id=123
POST /createOrder
PUT /updateOrder/456
DELETE /deleteOrder/456
Resource Naming
| Principle | Example |
|---|---|
| Use nouns, not verbs | /orders not /getOrders |
| Use plural forms | /users not /user |
| Use lowercase | /users not /Users |
| Use hyphens for readability | /user-accounts not /userAccounts |
HTTP Methods
| Method | Purpose | Idempotent | Safe |
|---|---|---|---|
| GET | Retrieve resource(s) | Yes | Yes |
| POST | Create resource | No | No |
| PUT | Replace resource | Yes | No |
| PATCH | Partial update | No | No |
| DELETE | Remove resource | Yes | No |
Request Design
Query Parameters
Use for filtering, sorting, and pagination:
GET /v1/orders?status=pending&sort=-created_at&page=2&limit=20
Conventions:
filter[field]=valuefor filteringsort=field(prefix-for descending)pageandlimitoroffsetandlimitfor paginationfields=field1,field2for sparse fieldsets
Request Bodies
Use JSON with consistent structure:
{
"data": {
"type": "order",
"attributes": {
"customer_id": "123",
"items": [
{
"product_id": "456",
"quantity": 2
}
]
}
}
}
Conventions:
- Use
snake_casefor property names (orcamelCase—just be consistent) - Wrap payloads in
dataenvelope if using JSON:API style - Include
typefor polymorphic resources
Response Design
Successful Responses
Single Resource:
{
"data": {
"id": "123",
"type": "order",
"attributes": {
"status": "pending",
"total": 99.99,
"created_at": "2024-01-15T10:30:00Z"
},
"relationships": {
"customer": {
"data": { "id": "456", "type": "customer" }
}
}
}
}
Collection:
{
"data": [...],
"meta": {
"total": 100,
"page": 2,
"limit": 20
},
"links": {
"self": "/v1/orders?page=2",
"first": "/v1/orders?page=1",
"prev": "/v1/orders?page=1",
"next": "/v1/orders?page=3",
"last": "/v1/orders?page=5"
}
}
Status Codes
| Code | Meaning | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST creating resource |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid request syntax or parameters |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Valid auth but insufficient permissions |
| 404 | Not Found | Resource doesn’t exist |
| 409 | Conflict | Request conflicts with current state |
| 422 | Unprocessable Entity | Valid syntax but semantic errors |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server-side failure |
Error Responses
Provide actionable error information:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid data",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Must be a valid email address"
},
{
"field": "quantity",
"code": "OUT_OF_RANGE",
"message": "Must be between 1 and 100"
}
],
"request_id": "req_abc123"
}
}
Authentication & Authorization
Authentication Methods
| Method | Use Case |
|---|---|
| API Keys | Server-to-server, simple integrations |
| OAuth 2.0 | User-authorized access, third-party apps |
| JWT | Stateless authentication, microservices |
Best Practices
- Use HTTPS exclusively—no exceptions
- Never expose credentials in URLs—use headers
- Implement proper token expiration—short-lived access tokens
- Use scopes for authorization—principle of least privilege
- Rate limit by client—prevent abuse
Header Conventions
Authorization: Bearer <token>
X-API-Key: <key>
X-Request-ID: <uuid>
Versioning
Strategies
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL Path | /v1/users | Explicit, easy routing | URL changes |
| Header | Accept: application/vnd.api+json;version=1 | Clean URLs | Hidden |
| Query Param | /users?version=1 | Simple | Caching issues |
Recommendation: URL path versioning for simplicity and explicitness.
Version Lifecycle
- Current: Actively developed and supported
- Deprecated: Still functional, no new features, sunset date announced
- Sunset: No longer available
Communicate version status in responses:
Deprecation: true
Sunset: Sat, 31 Dec 2024 23:59:59 GMT
Link: </v2/users>; rel="successor-version"
Pagination
Offset-Based
GET /v1/orders?page=2&limit=20
Response:
{
"data": [...],
"meta": {
"total": 100,
"page": 2,
"limit": 20,
"total_pages": 5
}
}
Best for: Stable datasets, random access needed
Cursor-Based
GET /v1/orders?limit=20&cursor=eyJpZCI6MTIzfQ==
Response:
{
"data": [...],
"meta": {
"has_more": true,
"next_cursor": "eyJpZCI6MTQzfQ=="
}
}
Best for: Large datasets, real-time data, infinite scroll
Rate Limiting
Headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1609459200
429 Response
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests",
"retry_after": 60
}
}
Include Retry-After header.
Documentation
What to Document
For each endpoint:
- HTTP method and URL
- Description of purpose
- Authentication requirements
- Request parameters (path, query, body)
- Request examples
- Response formats and examples
- Error codes and meanings
- Rate limits
OpenAPI Specification
Use OpenAPI (Swagger) for machine-readable documentation:
paths:
/v1/orders:
get:
summary: List orders
description: Returns a paginated list of orders
parameters:
- name: status
in: query
schema:
type: string
enum: [pending, completed, cancelled]
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/OrderList'
Security Checklist
- HTTPS only (redirect HTTP to HTTPS)
- Authentication on all non-public endpoints
- Input validation on all parameters
- Output encoding to prevent injection
- Rate limiting implemented
- CORS configured appropriately
- Security headers set (CSP, X-Frame-Options, etc.)
- Sensitive data not logged
- Audit logging for sensitive operations
- API keys/tokens rotatable
Performance Considerations
Caching
Use appropriate cache headers:
Cache-Control: max-age=3600, public
ETag: "abc123"
Last-Modified: Wed, 15 Jan 2024 10:30:00 GMT
Compression
Support gzip/deflate:
Accept-Encoding: gzip, deflate
Content-Encoding: gzip
Sparse Fieldsets
Allow clients to request only needed fields:
GET /v1/orders/123?fields=id,status,total
For API design review or architectural guidance, contact our team.