REST API Design Best Practices from 100+ Production APIs

Battle-tested API design principles, from versioning to authentication to error handling.

API architecture diagram

By Alex Kumar on

Building production APIs that scale, secure well, and are maintainable is an art honed over time. Here are lessons from building 100+ production APIs.

API Versioning

Best Practice: URL-based versioning

# Good
GET /api/v1/users
GET /api/v2/users

# Bad: Breaks existing clients
GET /api/users
GET /api/v2/users

Version lifecycle:

  • Support N-1 versions (v2 and v1 when v3 launches)
  • 6-month deprecation notice
  • Sunset policies documented

Resource Naming

Use nouns, not verbs:

# Good
GET /users/{id}
POST /users
PUT /users/{id}

# Bad: Verbs in URL
GET /getUsers
POST /createUser

Plural vs Singular:

  • Use plurals for collections: /users, /orders
  • Singular is acceptable only for single-resource operations

Request/Response Design

Consistent response format:

{
  "data": { ... },
  "meta": {
    "page": 1,
    "per_page": 50,
    "total": 500
  },
  "errors": null
}

Pagination:

GET /api/v1/users?page=2&per_page=50

# Cursor-based for large datasets
GET /api/v1/transactions?cursor=abc123&limit=100

Authentication & Authorization

JWT Best Practices:

# Set appropriate expiration
exp = datetime.utcnow() + timedelta(hours=1)

# Include minimal claims
payload = {
    "user_id": user.id,
    "role": user.role,
    "exp": exp
}

# Sign with strong secret
token = jwt.encode(payload, SECRET, algorithm="HS256")

API Keys for service-to-service:

  • Include in X-API-Key header
  • Rate limit per key
  • Rotate keys regularly
  • Scope permissions

Error Handling

HTTP Status Codes:

  • 200 OK - Success
  • 201 Created - Resource created
  • 204 No Content - Successful deletion
  • 400 Bad Request - Validation error
  • 401 Unauthorized - Authentication failed
  • 403 Forbidden - Authorization failed
  • 404 Not Found - Resource doesn’t exist
  • 409 Conflict - Resource state conflict
  • 422 Unprocessable Entity - Semantic error
  • 429 Too Many Requests - Rate limit
  • 500 Internal Server Error - Server error

Error Response Format:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Email is required",
    "details": {
      "field": "email",
      "constraint": "required"
    }
  }
}

Rate Limiting

Implement tiered rate limiting:

rate_limits = {
    "free": "100/hour",
    "pro": "1000/hour",
    "enterprise": "10000/hour"
}

# Use Redis for distributed rate limiting
# Return headers:
# X-RateLimit-Limit: 1000
# X-RateLimit-Remaining: 950
# X-RateLimit-Reset: 1638360000

Security Best Practices

Input Validation:

  • Validate all input at API boundaries
  • Use schema validation (Pydantic, Joi)
  • Sanitize user-generated content

CORS Configuration:

// Only allow trusted origins
const allowedOrigins = ['https://yourdomain.com']

app.use(cors({
  origin: function (origin, callback) {
    if (allowedOrigins.indexOf(origin) !== -1) {
      callback(null, true)
    } else {
      callback(new Error('CORS not allowed'))
    }
  }
}))

SQL Injection Prevention:

# Good: Parameterized queries
cursor.execute(
    "SELECT * FROM users WHERE id = %s",
    (user_id,)
)

# Bad: String concatenation
cursor.execute(
    f"SELECT * FROM users WHERE id = {user_id}"
)

Documentation

Use OpenAPI/Swagger:

openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
paths:
  /users:
    get:
      summary: List all users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: Successful response

Include for each endpoint:

  • Description
  • Authentication required
  • Parameters with examples
  • Request/response examples
  • Error codes
  • Rate limits

Testing Strategy

Three levels of testing:

  1. Unit Tests: Individual endpoints
def test_create_user():
    response = client.post('/api/v1/users', json={
        'name': 'John Doe',
        'email': 'john@example.com'
    })
    assert response.status_code == 201
  1. Integration Tests: Full workflows
def test_user_registration_flow():
    # Register
    response = client.post('/api/v1/register', json={...})
    # Login
    response = client.post('/api/v1/login', json={...})
    # Access protected resource
    response = client.get('/api/v1/profile', headers={...})
  1. Load Tests: Performance under stress
# Use Locust or k6
@task(100)
def get_users(l):
    l.client.get('/api/v1/users')

Monitoring & Observability

Key Metrics to Track:

  • Request rate (RPS)
  • Response time (p50, p95, p99)
  • Error rate
  • Rate limit hits
  • Database query times

Structured Logging:

{
  "timestamp": "2024-12-15T10:30:00Z",
  "level": "info",
  "request_id": "abc-123",
  "method": "GET",
  "path": "/api/v1/users",
  "status": 200,
  "duration_ms": 45
}

Performance Optimization

Caching Strategy:

# HTTP caching headers
@app.get("/api/v1/users/{id}")
@cache(ttl=300)  # 5 minutes
async def get_user(id: str):
    return await db.get_user(id)

Database Optimization:

  • Use indexes for filtered columns
  • Select only needed fields
  • Implement connection pooling
  • Use read replicas for reads

Compression:

# Compress responses > 1KB
if len(response.content) > 1024:
    response.headers['Content-Encoding'] = 'gzip'

Common Pitfalls to Avoid

N+1 Queries: Fetching related objects one by one ✅ Use eager loading or batch queries

Over-fetching: Returning all fields when only a few needed ✅ Allow field selection with ?fields=id,name

Inconsistent naming: user_id vs userId ✅ Use consistent naming convention (snake_case or camelCase)

No request validation: trusting client input ✅ Validate everything at API boundaries

Tight coupling: Database schema exposed in API ✅ Use DTOs and hide implementation details

Checklist for Production APIs

✅ Authentication and authorization ✅ Rate limiting ✅ Input validation and sanitization ✅ Proper HTTP status codes ✅ Comprehensive error handling ✅ API versioning ✅ CORS configuration ✅ Security headers (helmet, CSP) ✅ Request/response logging ✅ Metrics and monitoring ✅ Load testing completed ✅ Documentation complete ✅ SDK/Client libraries available

Conclusion

Good API design is about consistency, clarity, and developer experience. Follow REST principles, implement proper security, version your APIs, and document thoroughly. Your future self (and your API consumers) will thank you.

We have a newsletter

Subscribe and get the latest news and updates about AI & Backend Development on your inbox every week. No spam, no hassle.