| name | api-best-practices |
| description | REST API design patterns, OpenAPI specifications, versioning strategies, authentication, error handling, and security best practices. Use when designing APIs, creating endpoints, documenting APIs, or implementing backend services that expose HTTP APIs. |
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, DELETE201 Created: Successful POST with resource creation202 Accepted: Request accepted for async processing204 No Content: Successful DELETE or update with no response body
Client Errors (4xx):
400 Bad Request: Malformed request, validation error401 Unauthorized: Authentication required403 Forbidden: Authenticated but not authorized404 Not Found: Resource doesn't exist409 Conflict: Resource conflict (duplicate, version mismatch)422 Unprocessable Entity: Valid syntax but semantic errors429 Too Many Requests: Rate limit exceeded
Server Errors (5xx):
500 Internal Server Error: Unexpected server error502 Bad Gateway: Upstream service failure503 Service Unavailable: Temporary overload or maintenance504 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
1. Always Use HTTPS
Never send sensitive data over HTTP. Enforce HTTPS at the load balancer level.
2. 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)
3. Sanitize Outputs
Prevent injection attacks by escaping output:
import html
safe_output = html.escape(user_input)
4. 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}'")
5. 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
6. Authenticate Before Authorization
# 1. Verify JWT token (authentication)
# 2. Check user permissions (authorization)
# 3. Process request
7. Log Security Events
logger.warning(f"Failed login attempt for {email} from {ip_address}")
logger.critical(f"Privilege escalation attempt by user {user_id}")
8. 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
1. 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)
2. 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"
}
3. Use Compression
Accept-Encoding: gzip, deflate
Server should compress responses >1KB.
4. 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
5. 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.