API Best Practices
This skill provides comprehensive guidance for designing, implementing, and documenting RESTful APIs following industry best practices.
RESTful Design Principles
Resource-Oriented Design
APIs should be designed around resources (nouns), not actions (verbs):
Good:
GET /api/v1/users POST /api/v1/users GET /api/v1/users/{id} PUT /api/v1/users/{id} DELETE /api/v1/users/{id}
Bad:
GET /api/v1/getUsers POST /api/v1/createUser POST /api/v1/updateUser POST /api/v1/deleteUser
HTTP Methods and Their Meanings
-
GET: Retrieve a resource (safe, idempotent, cacheable)
-
POST: Create a new resource (not idempotent)
-
PUT: Replace entire resource (idempotent)
-
PATCH: Partial update (not necessarily idempotent)
-
DELETE: Remove a resource (idempotent)
HTTP Status Codes
Success (2xx):
-
200 OK : Successful GET, PUT, PATCH, DELETE
-
201 Created : Successful POST with resource creation
-
202 Accepted : Request accepted for async processing
-
204 No Content : Successful DELETE or update with no response body
Client Errors (4xx):
-
400 Bad Request : Malformed request, validation error
-
401 Unauthorized : Authentication required
-
403 Forbidden : Authenticated but not authorized
-
404 Not Found : Resource doesn't exist
-
409 Conflict : Resource conflict (duplicate, version mismatch)
-
422 Unprocessable Entity : Valid syntax but semantic errors
-
429 Too Many Requests : Rate limit exceeded
Server Errors (5xx):
-
500 Internal Server Error : Unexpected server error
-
502 Bad Gateway : Upstream service failure
-
503 Service Unavailable : Temporary overload or maintenance
-
504 Gateway Timeout : Upstream timeout
API Versioning
URL Versioning (Recommended)
GET /api/v1/users GET /api/v2/users
Pros: Clear, easy to route, visible in logs Cons: Duplicate code across versions
Header Versioning
GET /api/users Accept: application/vnd.myapi.v1+json
Pros: Clean URLs Cons: Harder to test, less visible
Version Management Rules
-
Never break backwards compatibility in same version
-
Deprecate old versions with advance notice (6-12 months)
-
Document migration guides between versions
-
Support at least 2 major versions simultaneously
Request/Response Patterns
Standard Request Format
JSON Request Body:
{ "email": "user@example.com", "name": "John Doe", "preferences": { "newsletter": true, "notifications": false } }
Query Parameters (for filtering, pagination, sorting):
GET /api/v1/users?role=admin&status=active&page=2&limit=20&sort=-created_at
Standard Response Format
Success Response:
{ "data": { "id": "user_123", "email": "user@example.com", "name": "John Doe", "createdAt": "2025-10-16T10:30:00Z" } }
Error Response:
{ "error": { "code": "INVALID_EMAIL", "message": "Email address is invalid", "field": "email", "details": "Email must contain @ symbol" } }
Collection Response with Pagination:
{ "data": [ { "id": 1, "name": "User 1" }, { "id": 2, "name": "User 2" } ], "pagination": { "page": 2, "limit": 20, "total": 156, "totalPages": 8, "hasNext": true, "hasPrev": true }, "links": { "self": "/api/v1/users?page=2", "next": "/api/v1/users?page=3", "prev": "/api/v1/users?page=1", "first": "/api/v1/users?page=1", "last": "/api/v1/users?page=8" } }
Authentication Patterns
JWT (JSON Web Tokens)
Login Flow:
POST /api/v1/auth/login { "email": "user@example.com", "password": "SecurePassword123" }
Response (200): { "accessToken": "eyJhbGc...", "refreshToken": "eyJhbGc...", "expiresIn": 900 }
Using Access Token:
GET /api/v1/users/me Authorization: Bearer eyJhbGc...
Token Refresh:
POST /api/v1/auth/refresh { "refreshToken": "eyJhbGc..." }
Response (200): { "accessToken": "eyJhbGc...", "expiresIn": 900 }
API Keys
Header-based (recommended):
GET /api/v1/data X-API-Key: sk_live_abc123xyz
Query parameter (less secure, use only for public data):
GET /api/v1/public-data?api_key=sk_live_abc123xyz
OAuth 2.0 Flows
Authorization Code Flow (for web apps):
-
Redirect to /oauth/authorize
-
User grants permission
-
Receive authorization code
-
Exchange code for access token at /oauth/token
-
Use access token for API requests
Client Credentials Flow (for server-to-server):
POST /oauth/token Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=abc&client_secret=xyz
Error Handling
Validation Errors
{ "error": { "code": "VALIDATION_ERROR", "message": "Request validation failed", "errors": [ { "field": "email", "message": "Email is required" }, { "field": "age", "message": "Age must be at least 18" } ] } }
Business Logic Errors
{ "error": { "code": "INSUFFICIENT_FUNDS", "message": "Account balance too low for this transaction", "details": { "balance": 50.00, "required": 100.00, "currency": "USD" } } }
Rate Limiting Errors
HTTP/1.1 429 Too Many Requests X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1634400000 Retry-After: 3600
{ "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "API rate limit exceeded", "retryAfter": 3600 } }
Pagination Strategies
Offset Pagination (Simple)
GET /api/v1/users?offset=40&limit=20
Pros: Simple, allows jumping to any page Cons: Performance degrades with large offsets, inconsistent if data changes
Cursor Pagination (Recommended for large datasets)
GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20
Response: { "data": [...], "pagination": { "nextCursor": "eyJpZCI6MTQzfQ", "hasMore": true } }
Pros: Consistent results, performant at any scale Cons: Can't jump to specific page
Page-Number Pagination (User-friendly)
GET /api/v1/users?page=3&limit=20
Pros: User-friendly, easy to understand Cons: Same issues as offset pagination
Rate Limiting
Implementation Pattern
Headers to include:
X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 999 X-RateLimit-Reset: 1634400000
Tiered Limits:
-
Anonymous: 100 requests/hour
-
Basic tier: 1,000 requests/hour
-
Pro tier: 10,000 requests/hour
-
Enterprise: Custom limits
Rate Limiting Algorithms
Token Bucket (recommended):
-
Allows bursts
-
Smooth long-term rate
-
Most flexible
Fixed Window:
-
Simple to implement
-
Can allow double limit at window boundaries
-
Less flexible
Sliding Window:
-
More accurate than fixed window
-
More complex to implement
-
Better user experience
API Security Best Practices
- Always Use HTTPS
Never send sensitive data over HTTP. Enforce HTTPS at the load balancer level.
- Validate All Inputs
from pydantic import BaseModel, EmailStr, constr
class UserCreate(BaseModel): email: EmailStr password: constr(min_length=8, max_length=100) name: constr(min_length=1, max_length=100)
- Sanitize Outputs
Prevent injection attacks by escaping output:
import html safe_output = html.escape(user_input)
- Use Parameterized Queries
✅ SAFE - Parameterized
cursor.execute("SELECT * FROM users WHERE email = ?", (email,))
❌ UNSAFE - String concatenation
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")
- Implement CORS Properly
Be specific with origins
CORS(app, origins=["https://myapp.com", "https://app.myapp.com"])
❌ NEVER use wildcard in production
CORS(app, origins=["*"]) # DANGEROUS
- Authenticate Before Authorization
1. Verify JWT token (authentication)
2. Check user permissions (authorization)
3. Process request
- Log Security Events
logger.warning(f"Failed login attempt for {email} from {ip_address}") logger.critical(f"Privilege escalation attempt by user {user_id}")
- Rate Limit Authentication Endpoints
Prevent brute force attacks:
-
/auth/login : 5 attempts per 15 minutes per IP
-
/auth/register : 3 attempts per hour per IP
-
/auth/reset-password : 3 attempts per hour per email
OpenAPI/Swagger Documentation
OpenAPI 3.0 Example
openapi: 3.0.0 info: title: My API version: 1.0.0 description: API for managing users and posts
servers:
- url: https://api.example.com/v1 description: Production server
- url: https://staging-api.example.com/v1 description: Staging server
paths: /users: get: summary: List users operationId: listUsers tags: - Users parameters: - name: page in: query schema: type: integer default: 1 - name: limit in: query schema: type: integer default: 20 maximum: 100 responses: '200': description: Successful response content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/User' pagination: $ref: '#/components/schemas/Pagination' post: summary: Create user operationId: createUser tags: - Users requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UserCreate' responses: '201': description: User created content: application/json: schema: $ref: '#/components/schemas/User' '400': description: Validation error content: application/json: schema: $ref: '#/components/schemas/Error'
components: schemas: User: type: object required: - id - email - name properties: id: type: string example: user_123 email: type: string format: email example: user@example.com name: type: string example: John Doe createdAt: type: string format: date-time example: 2025-10-16T10:30:00Z
UserCreate:
type: object
required:
- email
- password
- name
properties:
email:
type: string
format: email
password:
type: string
minLength: 8
maxLength: 100
name:
type: string
minLength: 1
maxLength: 100
Pagination:
type: object
properties:
page:
type: integer
limit:
type: integer
total:
type: integer
totalPages:
type: integer
hasNext:
type: boolean
hasPrev:
type: boolean
Error:
type: object
properties:
error:
type: object
properties:
code:
type: string
message:
type: string
details:
type: object
securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT
security:
- BearerAuth: []
API Endpoint Design Patterns
Collection and Resource Endpoints
Collection operations
GET /api/v1/posts # List posts POST /api/v1/posts # Create post GET /api/v1/posts/{id} # Get specific post PUT /api/v1/posts/{id} # Replace post PATCH /api/v1/posts/{id} # Update post DELETE /api/v1/posts/{id} # Delete post
Nested resources
GET /api/v1/posts/{id}/comments # List comments for post POST /api/v1/posts/{id}/comments # Create comment on post GET /api/v1/comments/{id} # Get specific comment DELETE /api/v1/comments/{id} # Delete comment
Action Endpoints (When REST Isn't Enough)
Sometimes you need RPC-style endpoints for actions:
POST /api/v1/users/{id}/verify-email POST /api/v1/orders/{id}/cancel POST /api/v1/posts/{id}/publish POST /api/v1/invoices/{id}/send
Pattern: POST /{resource}/{id}/{action}
Use when:
-
Action doesn't fit CRUD model
-
State transitions need to be explicit
-
Business logic requires specific endpoint
Request Validation
Input Validation Pattern
from pydantic import BaseModel, EmailStr, Field, validator
class UserCreate(BaseModel): email: EmailStr password: str = Field(min_length=8, max_length=100) name: str = Field(min_length=1, max_length=100) age: int = Field(ge=18, le=120)
@validator('password')
def password_strength(cls, v):
if not any(c.isupper() for c in v):
raise ValueError('Password must contain uppercase letter')
if not any(c.isdigit() for c in v):
raise ValueError('Password must contain digit')
return v
Validation Error Response
{ "error": { "code": "VALIDATION_ERROR", "message": "Request validation failed", "errors": [ { "field": "email", "message": "Email is required", "code": "REQUIRED_FIELD" }, { "field": "password", "message": "Password must contain uppercase letter", "code": "INVALID_FORMAT" } ] } }
Filtering, Sorting, Searching
Filtering
Single filter
GET /api/v1/posts?status=published
Multiple filters (AND)
GET /api/v1/posts?status=published&author=john
Multiple values (OR)
GET /api/v1/posts?tags=tech,ai,ml
Range filters
GET /api/v1/posts?created_after=2025-01-01&created_before=2025-12-31
Sorting
Single field ascending
GET /api/v1/posts?sort=created_at
Single field descending
GET /api/v1/posts?sort=-created_at
Multiple fields
GET /api/v1/posts?sort=-priority,created_at
Searching
Full-text search
GET /api/v1/posts?q=machine+learning
Field-specific search
GET /api/v1/posts?title=contains:machine&author=starts_with:john
Idempotency
Idempotent Operations (Safe to Retry)
-
GET, PUT, DELETE: Always idempotent
-
POST: Not idempotent by default
Idempotency Keys for POST
POST /api/v1/payments Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
{ "amount": 100.00, "currency": "USD", "description": "Payment for order #123" }
Server stores idempotency key:
-
First request: Process and return 201
-
Duplicate requests with same key: Return cached 201 response
-
Different request with same key: Return 409 Conflict
Async Operations
Long-Running Tasks
POST /api/v1/reports/generate { "type": "annual_summary", "year": 2025 }
Response (202 Accepted): { "id": "job_abc123", "status": "processing", "statusUrl": "/api/v1/jobs/job_abc123" }
Check Status
GET /api/v1/jobs/job_abc123
Response: { "id": "job_abc123", "status": "completed", "result": { "reportUrl": "/api/v1/reports/annual_summary_2025.pdf" }, "createdAt": "2025-10-16T10:00:00Z", "completedAt": "2025-10-16T10:05:00Z" }
Status values: queued , processing , completed , failed
Webhooks
Webhook Payload
{ "event": "user.created", "timestamp": "2025-10-16T10:30:00Z", "id": "evt_abc123", "data": { "id": "user_123", "email": "user@example.com", "name": "John Doe" } }
Webhook Security
HMAC Signature:
POST https://customer.com/webhooks X-Webhook-Signature: sha256=abc123...
Verify signature
import hmac import hashlib
def verify_webhook(payload, signature, secret): expected = hmac.new( secret.encode(), payload.encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(f"sha256={expected}", signature)
API Performance Best Practices
- Use ETags for Caching
GET /api/v1/users/123 ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Client sends If-None-Match on subsequent requests
GET /api/v1/users/123 If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Response: 304 Not Modified (if unchanged)
- Implement Field Selection
Get only specific fields
GET /api/v1/users/123?fields=id,email,name
Response: { "id": "user_123", "email": "user@example.com", "name": "John Doe" }
- Use Compression
Accept-Encoding: gzip, deflate
Server should compress responses >1KB.
- Batch Operations
Instead of N individual requests
GET /api/v1/users/1 GET /api/v1/users/2 GET /api/v1/users/3
Use batch endpoint
GET /api/v1/users?ids=1,2,3
- Database Query Optimization
-
Use database indexes on filter fields
-
Limit result set size (max 100 items per page)
-
Use connection pooling
-
Implement query caching for expensive queries
HATEOAS (Hypermedia)
Including Links in Responses
{ "data": { "id": "user_123", "email": "user@example.com", "name": "John Doe" }, "links": { "self": "/api/v1/users/123", "posts": "/api/v1/users/123/posts", "comments": "/api/v1/users/123/comments", "avatar": "/api/v1/users/123/avatar" } }
Benefits:
-
Self-documenting API
-
Clients discover available actions
-
API evolution easier
Content Negotiation
Request Format
Content-Type: application/json Accept: application/json
Support Multiple Formats (Optional)
Request JSON
Accept: application/json
Request XML
Accept: application/xml
Request CSV
Accept: text/csv
Deprecation Strategy
Announce Deprecation
GET /api/v1/old-endpoint Sunset: Sat, 31 Dec 2025 23:59:59 GMT Deprecation: Tue, 1 Oct 2025 00:00:00 GMT Link: </api/v2/new-endpoint>; rel="alternate"
Migration Guide
Provide clear migration path:
-
Announce deprecation 6-12 months in advance
-
Provide migration guide with code examples
-
Support old and new versions simultaneously
-
Monitor usage of deprecated endpoints
-
Send email notifications to API consumers
-
Finally remove deprecated endpoint
API Health and Status
Health Check Endpoint
GET /health
Response (200): { "status": "healthy", "version": "1.2.3", "timestamp": "2025-10-16T10:30:00Z" }
Readiness Check (Dependencies)
GET /health/ready
Response (200): { "status": "ready", "checks": { "database": "ok", "cache": "ok", "messageQueue": "ok", "externalAPI": "ok" } }
Response (503) if any dependency fails: { "status": "not_ready", "checks": { "database": "ok", "cache": "degraded", "messageQueue": "failed" } }
Testing APIs
Unit Testing Controllers
def test_create_user(): response = client.post("/api/v1/users", json={ "email": "test@example.com", "password": "SecurePass123", "name": "Test User" })
assert response.status_code == 201
assert response.json()["email"] == "test@example.com"
assert "password" not in response.json() # Never return passwords
Integration Testing
def test_user_flow(): # Create user response = client.post("/api/v1/users", json=user_data) user_id = response.json()["id"]
# Login
response = client.post("/api/v1/auth/login", json={
"email": user_data["email"],
"password": user_data["password"]
})
token = response.json()["accessToken"]
# Access protected resource
response = client.get(
f"/api/v1/users/{user_id}",
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
Common API Mistakes to Avoid
-
Using GET for state changes: GET should be safe and idempotent
-
Returning sensitive data: Never return passwords, tokens, secrets
-
Inconsistent naming: Stick to camelCase or snake_case, not both
-
Missing error details: Provide helpful error messages
-
No rate limiting: Always implement rate limits
-
Exposing internal IDs: Use UUIDs or slugs for public APIs
-
No versioning: Always version from day one
-
Ignoring CORS: Configure properly for web clients
-
Poor pagination: Implement cursor-based for large datasets
-
No documentation: Always provide OpenAPI docs
When to Use This Skill
Use this skill when:
-
Designing new API endpoints
-
Implementing REST APIs
-
Reviewing API code
-
Creating API documentation
-
Troubleshooting API issues
-
Discussing authentication/authorization
-
Planning API versioning strategy
-
Implementing rate limiting
-
Handling errors in APIs
Remember: A well-designed API is intuitive, secure, performant, and well-documented. Follow these patterns to create APIs that developers love to use.