| name | api-design |
| description | RESTful and GraphQL API design best practices. Use when designing new APIs, creating endpoint specifications, defining request/response schemas, setting up API versioning, or reviewing existing API designs for improvements. Triggers on tasks involving REST API design, GraphQL schema, API documentation, endpoint naming, error handling patterns, authentication/authorization flows, rate limiting, or API gateway configuration. |
| metadata | {"version":"1.0.0"} |
API Design Best Practices
A comprehensive guide for designing clean, consistent, and scalable APIs — covering REST conventions, GraphQL patterns, error handling, versioning, security, and documentation.
When to Apply
- Designing a new REST or GraphQL API from scratch
- Reviewing or refactoring existing API endpoints
- Defining API contracts between frontend and backend teams
- Setting up API documentation (OpenAPI / Swagger)
- Implementing authentication, authorization, or rate limiting
- Choosing between REST, GraphQL, gRPC, or WebSocket
1. Core Design Principles
1.1 Consistency Over Cleverness
Every API should feel like it was designed by one person. Establish conventions early and follow them ruthlessly.
Key rules:
- Pick one naming convention (snake_case or camelCase) and use it everywhere
- Pick one pluralization strategy for resources (always plural:
/users, not /user)
- Pick one error format and use it for every response
- Pick one pagination style and apply it to all list endpoints
1.2 Resource-Oriented Design (REST)
Model your API around resources (nouns), not actions (verbs).
| Good (Resource) | Bad (RPC-style) |
|---|
POST /orders | POST /createOrder |
GET /users/123 | GET /getUser?id=123 |
PUT /users/123/profile | POST /updateUserProfile |
DELETE /orders/456 | POST /deleteOrder?id=456 |
1.3 Use Standard HTTP Methods Correctly
| Method | Purpose | Idempotent | Safe |
|---|
GET | Read a resource | Yes | Yes |
POST | Create a resource / trigger action | No | No |
PUT | Full replacement of a resource | Yes | No |
PATCH | Partial update of a resource | No | No |
DELETE | Remove a resource | Yes | No |
2. URL Design & Naming
2.1 URL Structure
/{api-version}/{resource}/{resource-id}/{sub-resource}/{sub-resource-id}
Examples:
GET /v1/users # List users
GET /v1/users/123 # Get user 123
POST /v1/users # Create user
PUT /v1/users/123 # Replace user 123
PATCH /v1/users/123 # Partially update user 123
DELETE /v1/users/123 # Delete user 123
GET /v1/users/123/orders # List orders for user 123
POST /v1/users/123/orders # Create order for user 123
2.2 Query Parameters Convention
| Parameter | Purpose | Example |
|---|
filter | Field-level filtering | ?filter[status]=active&filter[role]=admin |
sort | Sorting | ?sort=created_at,-name (desc with -) |
page / per_page | Pagination | ?page=2&per_page=20 |
fields | Field selection (sparse fieldsets) | ?fields=id,name,email |
include | Relationship inclusion | ?include=orders,profile |
search | Full-text search | ?search=john doe |
2.3 Use Kebab-case for URLs
Good: /v1/user-profiles
Bad: /v1/userProfiles
Bad: /v1/user_profiles
3. Request & Response Design
3.1 Standard Response Envelope
{
"data": { ... },
"meta": {
"page": 1,
"per_page": 20,
"total": 150,
"total_pages": 8
},
"links": {
"self": "/v1/users?page=1",
"next": "/v1/users?page=2",
"last": "/v1/users?page=8"
}
}
3.2 Use Proper HTTP Status Codes
Success codes:
200 OK — Successful GET, PUT, PATCH, DELETE
201 Created — Successful POST (resource created)
204 No Content — Successful DELETE (no body needed)
Client error codes:
400 Bad Request — Malformed request syntax
401 Unauthorized — Missing or invalid authentication
403 Forbidden — Authenticated but not authorized
404 Not Found — Resource does not exist
409 Conflict — Request conflicts with current state
422 Unprocessable Entity — Validation errors
429 Too Many Requests — Rate limit exceeded
Server error codes:
500 Internal Server Error — Unexpected server error
502 Bad Gateway — Upstream service failure
503 Service Unavailable — Server temporarily unavailable
3.3 Validation Error Response Format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address",
"code": "INVALID_FORMAT"
},
{
"field": "age",
"message": "Must be greater than 0",
"code": "MIN_VALUE",
"min": 1
}
]
}
}
4. Error Handling
4.1 Standard Error Response
Every error response MUST follow this structure:
{
"error": {
"code": "MACHINE_READABLE_ERROR_CODE",
"message": "Human-readable error description",
"details": [],
"request_id": "req_abc123"
}
}
4.2 Error Code Naming Convention
Use SCREAMING_SNAKE_CASE with domain prefix:
AUTH_INVALID_TOKEN
AUTH_EXPIRED_CREDENTIALS
USER_NOT_FOUND
USER_ALREADY_EXISTS
ORDER_CANNOT_CANCEL
ORDER_INSUFFICIENT_STOCK
PAYMENT_DECLINED
RATE_LIMIT_EXCEEDED
VALIDATION_INVALID_FORMAT
VALIDATION_REQUIRED_FIELD
4.3 Never Expose Internal Errors
Incorrect:
{
"error": {
"message": "SQLException: Column 'emial' not found in table 'users'"
}
}
Correct:
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred. Please try again later.",
"request_id": "req_abc123"
}
}
5. API Versioning
5.1 Versioning Strategies
| Strategy | Example | Pros | Cons |
|---|
| URL path (Recommended) | /v1/users | Simple, visible, cacheable | URL changes |
| Header | Accept: application/vnd.api.v1+json | Clean URLs | Hidden, harder to test |
| Query param | /users?version=1 | Easy to add | Not RESTful |
5.2 Versioning Rules
- Always version from v1 (never v0)
- Use URL path versioning as the default strategy
- Maintain backward compatibility within a version
- Support at most 2 active versions simultaneously
- Provide a deprecation timeline (minimum 6 months) when sunsetting a version
- Include version in response headers:
API-Version: v1
6. Pagination
6.1 Cursor-Based Pagination (Recommended for large datasets)
{
"data": [...],
"pagination": {
"cursor": "eyJpZCI6MTAwfQ==",
"has_more": true,
"next_cursor": "eyJpZCI6MjAwfQ=="
}
}
Request: GET /v1/users?cursor=eyJpZCI6MTAwfQ==&limit=20
6.2 Offset-Based Pagination (Simple datasets)
Request: GET /v1/users?page=2&per_page=20
{
"data": [...],
"meta": {
"page": 2,
"per_page": 20,
"total": 150,
"total_pages": 8
}
}
6.3 When to Use Which
| Scenario | Strategy |
|---|
| Real-time feeds, infinite scroll | Cursor-based |
| Static datasets, export | Offset-based |
| Large datasets (>10k records) | Cursor-based |
| Admin dashboards with total count | Offset-based |
7. Authentication & Authorization
7.1 Authentication Patterns
| Method | Use Case | Header Format |
|---|
| Bearer Token (JWT) | User authentication | Authorization: Bearer <token> |
| API Key | Service-to-service | X-API-Key: <key> |
| OAuth 2.0 | Third-party access | Authorization: Bearer <token> |
7.2 Security Checklist
8. Rate Limiting
8.1 Standard Rate Limit Headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1620000000
Retry-After: 30
8.2 Rate Limit Response (429)
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please retry after 30 seconds.",
"retry_after": 30
}
}
9. GraphQL-Specific Guidelines
9.1 Schema Design
- Use PascalCase for types and enums
- Use camelCase for fields and arguments
- Always provide a
description for every type and field
- Use
Input types for complex mutation arguments
- Implement cursor-based connections for list fields
9.2 Naming Conventions
type User {
id: ID!
email: String!
fullName: String!
createdAt: ISO8601DateTime!
}
input CreateUserInput {
email: String!
fullName: String!
password: String!
}
type CreateUserPayload {
user: User!
token: String!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}
9.3 Error Handling in GraphQL
Use a union type or error extension pattern:
union CreateUserResult = CreateUserSuccess | ValidationError | AuthenticationError
type Mutation {
createUser(input: CreateUserInput!): CreateUserResult!
}
10. API Documentation Checklist
11. Quick Reference: API Design Anti-Patterns
| Anti-Pattern | Problem | Solution |
|---|
| Verbs in URLs | RPC-style, not RESTful | Use nouns + HTTP methods |
GET with body | Not cached, not idempotent | Move data to query params |
| Inconsistent error formats | Hard to handle client-side | Use one standard error format |
| No pagination on lists | Performance degradation | Always paginate list endpoints |
| Returning 200 for errors | Misleading status codes | Use proper HTTP status codes |
| Nested resources > 2 levels | Complex URLs | Use query params or redesign |
| No request IDs | Impossible to debug | Include X-Request-ID header |
| Version in body only | Not visible in logs | Version in URL or header |