APIs are contracts. Once published, changing them breaks consumers. The cost of API mistakes compounds over time as more clients depend on poor decisions. Getting API design right matters more than getting implementation right—you can refactor implementation, but API changes require coordinating with everyone who depends on you.
These principles have guided my API design across dozens of services. They’re not revolutionary; they’re distilled from hard-won experience and the accumulated wisdom of the developer community.
Consistency Above All
The single most important quality of an API is consistency. A consistent API is predictable; developers learn patterns once and apply them everywhere. An inconsistent API forces developers to check documentation for every endpoint, remember special cases, and make mistakes.
Naming Consistency
Choose naming conventions and apply them everywhere:
- Resource names: plural or singular? Pick one. (
/usersnot sometimes/usersand sometimes/user) - Case conventions: camelCase or snake_case? Pick one for URLs, one for JSON fields, stick to them.
- Verbs in URLs: generally avoid them for REST APIs.
/users/123/activatevs POST to/users/123/active. Have a policy.
Behavior Consistency
- How do you return errors? Same structure, same HTTP status codes for the same types of failures.
- How do you paginate? Same pattern across all list endpoints.
- How do you handle null values? Omit keys, or include with null? Pick one.
Developers will assume that patterns they’ve seen apply universally. When they don’t, developers debug phantom issues caused by violated assumptions.
Use HTTP Correctly
REST APIs should leverage HTTP semantics. Clients and infrastructure understand HTTP; fighting it creates confusion.
HTTP Methods
- GET: Retrieve resources. Safe and idempotent. Never modify state on GET.
- POST: Create resources or trigger actions. Not idempotent by default.
- PUT: Replace resources entirely. Idempotent.
- PATCH: Partial resource updates. May or may not be idempotent depending on implementation.
- DELETE: Remove resources. Idempotent (deleting twice should succeed, not 404 on second attempt).
HTTP Status Codes
Use appropriate status codes:
- 200: Success with response body
- 201: Resource created (include Location header)
- 204: Success with no response body
- 400: Client error (bad request format, validation failure)
- 401: Authentication required
- 403: Authenticated but not authorized
- 404: Resource not found
- 409: Conflict (resource state conflict)
- 422: Semantic errors (validation that’s not just format)
- 500: Server error (not the client’s fault)
Don’t use 200 for everything with error information in the body. Clients and infrastructure depend on status codes for routing, caching, and error handling.
HTTP Headers
Leverage standard headers:
Content-Type: Always specify for requests and responsesAccept: Honor content negotiationCache-Control: Enable caching where appropriateETag/If-None-Match: Conditional requests save bandwidthLocation: Return for created resources
Custom headers should use X- prefix (though this convention is deprecated, it remains common) or a vendor-specific prefix.
Design for Evolvability
APIs must evolve as requirements change. Design for change from the start.
Versioning Strategy
Decide on versioning early:
URL versioning: /v1/users vs /v2/users. Clear and explicit. Clients see the version in every request.
Header versioning: Accept: application/vnd.api+json;version=1. Keeps URLs clean but is less visible.
No versioning: Make all changes backward-compatible. Simplest when achievable, but eventually you’ll need breaking changes.
My recommendation: URL versioning for its clarity and tooling compatibility. Accept that URLs will include version segments.
Backward Compatibility
Adding fields to responses is backward compatible; removing fields breaks clients. Adding optional request parameters is compatible; making parameters required breaks clients.
Changes that seem minor can break consumers:
- Changing field types (number to string)
- Changing null handling
- Changing error response format
- Changing pagination behavior
Document what constitutes a breaking change in your versioning policy.
Deprecation
When deprecating endpoints or fields:
- Mark as deprecated in documentation
- Add deprecation response headers (
Deprecation,Sunset) - Log usage to understand who depends on deprecated features
- Provide migration timeline
- Communicate directly with high-volume consumers
Never remove without warning. The sunset period should be proportional to your API’s stability promises.
Error Handling
Good error responses help developers fix problems quickly. Bad error responses waste hours of debugging time.
Error Structure
Provide consistent, useful error responses:
{
"error": {
"code": "validation_error",
"message": "Request validation failed",
"details": [
{
"field": "email",
"code": "invalid_format",
"message": "Email must be a valid email address"
},
{
"field": "age",
"code": "out_of_range",
"message": "Age must be between 0 and 150"
}
],
"request_id": "req_abc123"
}
}
Include:
- Machine-readable error codes (not just messages)
- Human-readable messages
- Field-level detail for validation errors
- Request IDs for support and debugging
Error Messages
Error messages should be:
- Specific: “Email must be a valid email address” not “Invalid value”
- Actionable: Tell developers what to fix
- Safe: Don’t leak internal details or stack traces in production
Rate Limiting
When rate limiting, help clients adapt:
- Return
429 Too Many RequestswithRetry-Afterheader - Include rate limit information in response headers:
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset - Document rate limits clearly
Pagination
Any endpoint returning lists must support pagination. Even if your list is small today, it will grow.
Cursor-Based Pagination
Cursor pagination provides stable results even when data changes:
GET /users?limit=20&cursor=eyJpZCI6MTIzfQ
Response includes next cursor:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTQzfQ",
"has_more": true
}
}
Cursors are opaque to clients—they shouldn’t parse or construct them. This lets you change cursor implementation without breaking clients.
Offset-Based Pagination
Simpler but has edge cases:
GET /users?limit=20&offset=40
Problems: if items are added/removed between requests, clients may miss items or see duplicates. For frequently-changing data, cursor pagination is safer.
Include Total Counts Carefully
Clients often want total counts for UI. But counting can be expensive at scale. Consider:
- Making counts optional (
?include=count) - Returning approximate counts
- Caching counts with staleness disclaimer
- Providing counts only for filtered results below a threshold
Security
API security goes beyond authentication.
Authentication
Support industry-standard authentication:
- API keys: Simple, suitable for server-to-server. Transmit in headers, not URLs (URLs get logged).
- OAuth 2.0: Standard for user-authorized access. Support appropriate grant types for your use cases.
- JWT: Stateless tokens for distributed systems. Validate signatures and expiration properly.
Authorization
Authentication proves identity; authorization controls access. Implement both:
- Scope-based access control for OAuth tokens
- Resource ownership verification
- Role-based permissions where appropriate
Input Validation
Validate all input:
- Type checking
- Range validation
- Length limits
- Format validation (emails, URLs, etc.)
Reject invalid input early with clear error messages. Never trust client input.
Output Filtering
Don’t leak sensitive data:
- Filter internal fields before response
- Respect authorization in response content (don’t return data user can’t access)
- Consider field-level permissions for sensitive data
Documentation
Even perfect APIs fail without good documentation.
What to Document
- Authentication methods with examples
- Every endpoint with request/response examples
- All parameters with types, constraints, and defaults
- All response codes with meanings
- Error response formats
- Rate limits and quotas
- Versioning policy
- Deprecation procedures
Documentation Formats
OpenAPI (Swagger) provides machine-readable documentation that can generate client libraries and interactive documentation. Maintain it alongside implementation.
Supplement OpenAPI with guides: getting started, common use cases, best practices. Reference documentation explains what’s possible; guides explain what to do.
Keep Documentation Current
Stale documentation is worse than no documentation—it creates false confidence. Integrate documentation updates into your development process. Test that examples work. Review documentation in code review.
Practical Advice
Start Minimal
Launch with the smallest API that serves your needs. Every endpoint is a commitment; every field is a commitment. It’s easier to add than to remove.
Design for Clients
Think about how clients will use your API, not how your implementation works. The goal is client productivity, not exposing your database schema.
Get Feedback Early
Before finalizing your API, have developers use it. Build a client yourself. Friction in early usage reveals design problems cheaper to fix before launch.
Monitor Usage
Log API calls to understand how clients actually use your API. This informs evolution decisions: what to deprecate, what to add, what to optimize.
Key Takeaways
- Consistency is the most important API quality; establish conventions and follow them everywhere
- Use HTTP semantics correctly: appropriate methods, status codes, and headers
- Design for evolvability with versioning, backward compatibility, and deprecation policies
- Provide useful error responses with machine-readable codes and actionable messages
- Implement pagination from the start; prefer cursor-based for stability
- Document thoroughly and keep documentation current
- Start minimal and expand based on real usage