| name | api-design |
| description | Use when the user asks to "design an API", "review an API", "choose between REST and GraphQL", "version an API", "design a webhook", or discusses API contracts, backwards compatibility, or client-server interfaces. Provides senior engineering guidance for API design decisions. |
API Design
APIs are contracts. A published API is a promise to every consumer — breaking it without warning breaks trust and breaks systems. Good API design means designing for the client first, versioning explicitly from day one, and treating backwards compatibility as a hard constraint.
When to Activate
- Designing a new API from scratch
- Reviewing an existing API for consistency or usability
- Choosing between REST, GraphQL, and gRPC
- Versioning an API that needs to change
- Designing webhooks or event-driven interfaces
- Evaluating backwards compatibility of a proposed change
Choosing a Protocol
REST
Use when:
- Consumers are diverse and unknown (public API)
- Resources map naturally to HTTP semantics (CRUD on entities)
- Cacheability of responses is valuable
- Team has broad HTTP knowledge
Tradeoffs: over-fetching/under-fetching on complex queries; requires multiple round-trips for related resources.
GraphQL
Use when:
- Clients need flexible, precise data fetching (mobile apps, varied clients)
- Data is highly relational and clients need graph traversal
- You own both client and server
Tradeoffs: complex caching (queries are not cacheable by URL), N+1 resolver problem requires DataLoader, introspection exposure, higher server complexity.
gRPC
Use when:
- Service-to-service communication in a controlled environment
- High throughput, low latency is required
- Strong typing and code generation across services matters
- Streaming (server-side, client-side, bidirectional) is needed
Tradeoffs: poor browser support, harder to debug than JSON, requires protobuf knowledge.
Default: REST for external-facing APIs. gRPC for internal service mesh. GraphQL when clients are complex and diverse.
REST API Design Principles
Resource Naming
- Use nouns, not verbs:
/orders not /getOrders
- Plural for collections:
/users, /orders
- Nest for clear ownership:
/users/{id}/orders (orders belong to a user)
- Avoid deep nesting (>2 levels):
/users/{id}/orders/{id}/items/{id} → consider /order-items?order_id=
- Lowercase with hyphens:
/payment-methods not /paymentMethods
HTTP Method Semantics
| Method | Semantics | Idempotent | Safe |
|---|
| GET | Retrieve | Yes | Yes |
| POST | Create (non-idempotent) | No | No |
| PUT | Replace (full update) | Yes | No |
| PATCH | Partial update | No | No |
| DELETE | Remove | Yes | No |
POST is for operations that are not naturally idempotent. Make them idempotent by accepting an idempotency key header.
Status Codes
Use the correct code — do not return 200 OK for an error:
| Code | Use |
|---|
| 200 OK | Successful GET, PUT, PATCH |
| 201 Created | Successful POST that created a resource |
| 204 No Content | Successful DELETE or PUT with no body |
| 400 Bad Request | Client sent invalid input (validation error) |
| 401 Unauthorized | Not authenticated |
| 403 Forbidden | Authenticated but not authorized |
| 404 Not Found | Resource does not exist |
| 409 Conflict | State conflict (duplicate, optimistic lock) |
| 422 Unprocessable Entity | Syntactically valid but semantically invalid |
| 429 Too Many Requests | Rate limited |
| 500 Internal Server Error | Unexpected server failure |
| 503 Service Unavailable | Temporary unavailability, retry later |
Error Response Format
Consistent error bodies make APIs usable. Include:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "The request could not be processed",
"details": [
{ "field": "email", "issue": "must be a valid email address" }
],
"request_id": "req_abc123"
}
}
code: machine-readable, stable across versions
message: human-readable, may change
details: field-level errors for validation failures
request_id: traceable to server logs
Pagination
For collections, always paginate. Two patterns:
Cursor-based (preferred for large, real-time datasets):
GET /events?cursor=eyJpZCI6MTAwfQ&limit=50
Response: { "data": [...], "next_cursor": "eyJpZCI6MTUwfQ", "has_more": true }
Offset-based (simpler, for stable datasets):
GET /products?page=3&per_page=25
Response: { "data": [...], "total": 1000, "page": 3, "per_page": 25 }
Use cursor-based for anything that changes in real time. Offset pagination on a changing dataset produces gaps and duplicates.
Versioning
Version your API before you need to. Changing an unversioned API breaks consumers silently.
URI versioning (most explicit): /v1/users, /v2/users
Header versioning: API-Version: 2024-01-01
URI versioning is preferred for discoverability. Header versioning keeps URLs clean.
Backwards Compatibility Rules
Non-breaking changes (safe to make without a new version):
- Adding a new endpoint
- Adding an optional field to a response
- Adding an optional query parameter
- Adding a new status code to an existing endpoint
Breaking changes (require a new version):
- Removing a field from a response
- Renaming a field
- Changing a field's type
- Changing behavior of an existing endpoint
- Removing an endpoint
When in doubt, treat the change as breaking.
Deprecation Policy
Before removing anything:
- Mark as deprecated in documentation and response headers:
Deprecation: true, Sunset: Sat, 01 Jan 2027 00:00:00 GMT
- Communicate to known consumers
- Monitor usage — do not sunset while traffic exists
- Maintain the old version for at least 6-12 months after deprecation
Authentication and Authorization
Authentication: verify identity
- API keys: simple, good for server-to-server
- OAuth 2.0 + JWT: for user-delegated access
- mTLS: for service-to-service in high-security environments
Authorization: verify permission
- Attach authorization to every resource operation, not just endpoints
- Return
403 Forbidden, never 404 Not Found for authorization failures — unless existence itself should be hidden (e.g., admin-only resources)
- Apply the principle of least privilege: a token should have only the scopes it needs
Webhooks
Design outbound webhooks with:
- Signed payloads: HMAC-SHA256 signature in a header so consumers can verify authenticity
- Retry with backoff: treat delivery failures as transient; retry with exponential backoff
- Idempotency keys: include a unique event ID so consumers can deduplicate retries
- Event schema stability: webhook payloads are an API contract — apply the same versioning rules
Gotchas
-
Chatty APIs that require many calls: if clients consistently call 5 endpoints to render one screen, the API does not match the client's mental model. Consider composite endpoints or GraphQL for complex clients.
-
Inconsistent naming conventions: mixing camelCase and snake_case in the same API signals a lack of ownership. Enforce a single convention and lint for it.
-
No rate limiting on public endpoints: any endpoint that does work will be abused. Implement rate limiting from day one.
-
Silent 200s for errors: returning { "status": "error" } with a 200 OK status code breaks every HTTP client that routes on status code. Use the correct status.
-
Leaking internal identifiers: auto-increment database IDs tell consumers your volume and let attackers enumerate resources. Use UUIDs or opaque tokens for external-facing IDs.
-
No request ID: without a request_id in responses, debugging production issues requires guessing. Generate a unique ID per request and include it in all responses and logs.
-
Forgetting idempotency on mutations: a POST with no idempotency support means a network retry charges a card twice. Add idempotency key support to all non-safe, non-idempotent operations.
Integration
- system-design — APIs are the interface between services you design
- security-engineering — apply authentication, authorization, and input validation to every API
- testing-strategy — API contracts should have contract tests
- code-review — review API changes for backwards compatibility before merge
References
Skill Metadata
Created: 2026-04-10
Version: 1.0.0