The Ultimate API Design Checklist for Production Apps
Ship production-ready APIs with this comprehensive checklist. Covers REST design, authentication, validation, error handling, rate limiting, versioning, documentation, and monitoring.
Shipping an API to production is deceptively hard. The endpoint that works perfectly on localhost has a way of crumbling the moment real users, hostile bots, and unpredictable network conditions enter the picture. Authentication edge cases, inconsistent error formats, missing rate limits, zero observability β any one of these gaps can turn a launch into an incident.
This guide is the checklist we wish every backend team had pinned to their wall before cutting the first release. It covers twelve categories, from URL design to testing strategy, with concrete examples and actionable items you can tick off one by one. Whether you are building a public REST API, an internal microservice contract, or a third-party integration layer, these principles apply.
Bookmark this page, share it with your team, and work through it before your next deployment. If you want to validate your endpoints as you go, test your APIs instantly with our free API Request Builder.
1. URL Design & Resource Naming
Your URL structure is the first thing consumers see. A well-designed URL scheme is self-documenting and dramatically reduces support requests.
Checklist
- Use nouns, not verbs, for resource paths (
/users, not/getUsers) - Keep URLs lowercase with hyphens for multi-word segments (
/order-items, not/orderItems) - Use plural nouns for collections (
/products,/invoices) - Nest resources to express relationships, but limit depth to two levels (
/users/42/orders, not/users/42/orders/7/items/3/reviews) - Avoid file extensions in URLs (
.json,.xml); use theAcceptheader instead - Use query parameters for filtering, sorting, and pagination (
/products?category=electronics&sort=-price) - Reserve a consistent prefix for API routes (
/api/v1/...)
Examples
GET /api/v1/users # List users
GET /api/v1/users/42 # Get a single user
POST /api/v1/users # Create a user
PATCH /api/v1/users/42 # Partial update
DELETE /api/v1/users/42 # Delete a user
GET /api/v1/users/42/orders # List orders for user 42
A common mistake is encoding actions into the URL (/api/v1/users/42/activate). Instead, model state changes as resource updates:
PATCH /api/v1/users/42
Content-Type: application/json
{ "status": "active" }
If the action truly does not map to a CRUD operation β like sending an email or triggering a build β use a dedicated action resource (/api/v1/users/42/actions/send-verification). Keep these the exception, not the rule.
2. HTTP Methods & Status Codes
Using the correct HTTP method and status code is not pedantic β it determines how caches, proxies, browsers, and client libraries behave.
Methods Checklist
-
GETis safe and idempotent β never use it to mutate state -
POSTcreates a new resource; return201 Createdwith aLocationheader -
PUTreplaces the entire resource;PATCHapplies a partial update -
DELETEremoves the resource; return204 No Contenton success - Support
OPTIONSfor CORS preflight (see Security section) - Return
405 Method Not Allowedfor unsupported methods on a valid resource
Status Codes Checklist
-
200 OKfor successful GET, PUT, PATCH -
201 Createdfor successful POST withLocationheader -
204 No Contentfor successful DELETE or updates with no response body -
400 Bad Requestfor malformed syntax or validation errors -
401 Unauthorizedwhen authentication is missing or invalid -
403 Forbiddenwhen authenticated but lacking permission -
404 Not Foundfor nonexistent resources -
409 Conflictfor duplicate creation or state conflicts -
422 Unprocessable Entityfor semantically invalid input -
429 Too Many Requestsfor rate-limited clients -
500 Internal Server Erroras the catch-all server fault -
503 Service Unavailableduring maintenance or overload
Never return 200 OK with an error message buried in the body. Clients rely on status codes for control flow β respect the contract.
3. Authentication & Authorization
Security is not a feature you bolt on later. Every production API needs a clear authentication and authorization strategy from day one.
Checklist
- Enforce HTTPS everywhere β reject plain HTTP requests with
301or403 - Choose an auth mechanism appropriate to your consumers:
- API keys for server-to-server or simple integrations
- OAuth 2.0 + PKCE for user-facing apps and third-party access
- JWT (JSON Web Tokens) for stateless, short-lived session tokens
- Store API keys and secrets securely β never in source code or client bundles
- Use short-lived access tokens (15-60 minutes) with refresh token rotation
- Validate JWT signatures on every request; check
exp,iss,audclaims - Implement scope-based authorization (e.g.,
read:users,write:orders) - Return
401for missing/invalid credentials,403for insufficient permissions - Rate-limit authentication endpoints aggressively to prevent brute force
- Log all authentication failures for anomaly detection
JWT Validation Example
// Decoded JWT payload
{
"sub": "user_42",
"iss": "https://auth.example.com",
"aud": "https://api.example.com",
"exp": 1742400000,
"iat": 1742396400,
"scopes": ["read:users", "write:orders"]
}
# Request with Bearer token
curl -X GET https://api.example.com/api/v1/users/42 \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
A critical but often overlooked step: always validate the token on the server side. Do not trust the clientβs decoded payload β verify the cryptographic signature against your public key or JWKS endpoint.
For a deeper dive into securing your full stack, check out the Fullstack Boilerplate Collection which includes pre-configured auth setups for Node.js, Python, and Go.
4. Request Validation & Input Sanitization
Every byte of input that crosses your API boundary is untrusted. Validate early, validate thoroughly, and fail fast with clear messages.
Checklist
- Validate all required fields are present before processing
- Enforce data types (string, number, boolean, array, object)
- Set maximum string lengths to prevent payload abuse
- Validate enum values against an allowlist
- Sanitize strings to prevent SQL injection, XSS, and command injection
- Reject unexpected fields (use strict schema validation, not permissive)
- Validate content types β reject requests without proper
Content-Typeheader - Set a maximum request body size (e.g., 1 MB for JSON, configurable for file uploads)
- Validate path parameters and query strings, not just request bodies
- Use a schema validation library (Joi, Zod, Pydantic, JSON Schema) rather than hand-written checks
Validation Error Response
When validation fails, return all errors at once so the client can fix everything in a single retry:
{
"status": 422,
"error": "Validation Failed",
"message": "The request body contains invalid fields.",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Must be a valid email address."
},
{
"field": "age",
"code": "OUT_OF_RANGE",
"message": "Must be between 13 and 150."
},
{
"field": "role",
"code": "INVALID_ENUM",
"message": "Must be one of: admin, editor, viewer."
}
]
}
This approach saves your consumers from the frustrating cycle of fixing one error, resubmitting, hitting the next error, and repeating.
5. Error Handling & Response Format
Consistent error responses are the difference between an API that developers love and one they dread. Standardize your error format across every endpoint.
Checklist
- Use a single, consistent error response schema across the entire API
- Include a machine-readable error code (not just the HTTP status)
- Include a human-readable message suitable for logs and developer debugging
- Never expose stack traces, internal paths, or database details in production
- Include a
request_idortrace_idfor correlating errors with server logs - Provide a documentation link for each error code when possible
- Handle unexpected exceptions with a generic 500 response and log the full details server-side
- Return errors in the same content type the client requested (JSON for JSON APIs)
Standard Error Envelope
{
"status": 404,
"error": "RESOURCE_NOT_FOUND",
"message": "No user found with ID 42.",
"request_id": "req_abc123def456",
"documentation_url": "https://docs.example.com/errors/RESOURCE_NOT_FOUND"
}
Standard Success Envelope
Consistency applies to success responses too. Wrapping data in an envelope makes pagination metadata and future extensions painless:
{
"status": 200,
"data": {
"id": 42,
"name": "Jane Doe",
"email": "[email protected]"
},
"meta": {
"request_id": "req_abc123def456"
}
}
Want to test how your API handles various error scenarios? Our API Request Builder lets you craft custom requests and inspect full responses, headers included.
6. Pagination, Filtering & Sorting
Any endpoint that returns a list will eventually return thousands of items. Plan for it now, not after your database falls over.
Checklist
- Default to paginated responses for all list endpoints
- Support cursor-based pagination for large or frequently changing datasets
- Support offset-based pagination (
?page=2&per_page=25) for simpler use cases - Set a sensible default page size (e.g., 20) and a maximum (e.g., 100)
- Return pagination metadata in the response body (
total_count,next_cursor,has_more) - Include
Linkheaders withrel="next"andrel="prev"for discoverability - Support field-level filtering via query parameters (
?status=active&role=admin) - Support sorting with a consistent syntax (
?sort=created_ator?sort=-pricefor descending) - Support multi-field sorting (
?sort=-priority,created_at) - Document which fields are filterable and sortable
Cursor-Based Pagination Response
{
"status": 200,
"data": [
{ "id": 101, "name": "Widget A", "price": 29.99 },
{ "id": 102, "name": "Widget B", "price": 49.99 }
],
"pagination": {
"next_cursor": "eyJpZCI6MTAyfQ==",
"has_more": true,
"per_page": 20
}
}
# Fetch next page using cursor
curl "https://api.example.com/api/v1/products?cursor=eyJpZCI6MTAyfQ==&per_page=20" \
-H "Authorization: Bearer <token>"
Cursor-based pagination outperforms offset pagination on large tables because it avoids the OFFSET N query, which forces the database to scan and discard rows. Use offset only when you need random page access (page 1, page 50) and the dataset is small enough to handle it.
7. Rate Limiting & Throttling
Without rate limiting, a single runaway client can take down your entire service. Protect yourself and your other users.
Checklist
- Implement rate limiting on all public endpoints
- Use the
RateLimit-Limit,RateLimit-Remaining, andRateLimit-Resetresponse headers (draft IETF standard) - Return
429 Too Many Requestswith aRetry-Afterheader when the limit is exceeded - Apply stricter limits to authentication and password reset endpoints
- Use sliding window or token bucket algorithms for smooth limiting
- Rate limit by API key, user ID, or IP address depending on your auth model
- Consider tiered rate limits (free: 100 req/min, pro: 1000 req/min)
- Implement a global circuit breaker for downstream service failures
- Log rate-limited requests for capacity planning
- Document rate limits clearly in your API documentation
Rate Limit Response Headers
HTTP/1.1 200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 73
RateLimit-Reset: 1742400060
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json
{
"status": 429,
"error": "RATE_LIMIT_EXCEEDED",
"message": "You have exceeded 100 requests per minute. Please retry after 30 seconds.",
"retry_after": 30
}
8. Versioning Strategy
APIs evolve. Breaking changes are inevitable. A clear versioning strategy lets you ship improvements without breaking existing clients.
Checklist
- Choose a versioning strategy and apply it consistently:
- URL path versioning (
/api/v1/users) β most common, easiest to understand - Header versioning (
Accept: application/vnd.example.v2+json) β cleaner URLs, harder to test - Query parameter versioning (
/api/users?version=2) β simple but can be ignored
- URL path versioning (
- Start with v1, not v0 β v0 signals instability
- Maintain at least one previous version during migration periods
- Set a deprecation policy (e.g., 6-12 months notice before sunsetting a version)
- Return a
Deprecationheader on deprecated endpoints - Communicate breaking changes via changelog, email, and dashboard notices
- Use semantic versioning for SDKs and client libraries
- Never break a released version β additions are fine, removals are not
Deprecation Header
HTTP/1.1 200 OK
Deprecation: Sun, 01 Sep 2026 00:00:00 GMT
Sunset: Mon, 01 Dec 2026 00:00:00 GMT
Link: <https://docs.example.com/migration/v1-to-v2>; rel="deprecation"
A practical rule of thumb: if you can make the change additive (adding a new field, a new endpoint, a new optional parameter), do that instead of creating a new version. Reserve version bumps for genuinely breaking changes like removing fields, renaming resources, or changing response shapes.
9. Documentation (OpenAPI / Swagger)
An API without documentation is an API nobody wants to use. Machine-readable specs enable tooling, code generation, and automated testing.
Checklist
- Write an OpenAPI 3.x specification as the single source of truth
- Document every endpoint, including request/response schemas, headers, and status codes
- Include realistic example values in your schemas
- Host interactive documentation (Swagger UI, Redoc, or Stoplight)
- Include authentication setup instructions with working examples
- Document rate limits, pagination behavior, and error codes
- Provide a quickstart guide with copy-paste curl commands
- Keep documentation in version control, updated with every PR
- Auto-generate client SDKs from the OpenAPI spec where possible
- Include a changelog that is updated with every release
Minimal OpenAPI Example
openapi: 3.0.3
info:
title: Example API
version: 1.0.0
description: A production-ready example API.
paths:
/api/v1/users:
get:
summary: List all users
operationId: listUsers
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: per_page
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: A paginated list of users
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
pagination:
$ref: '#/components/schemas/Pagination'
Good documentation pays for itself. Every hour you spend writing docs saves hundreds of hours of support questions, Slack messages, and frustrated developers reading your source code to figure out what your API does.
For ready-to-use API testing workflows that complement your docs, grab the API Testing Cheatsheet Pack.
10. Monitoring, Logging & Alerting
You cannot fix what you cannot see. Production APIs need observability built in from the start, not bolted on after the first outage.
Checklist
- Log every request with: timestamp, method, path, status code, response time, request ID
- Use structured logging (JSON) for machine parseability
- Include correlation IDs (
X-Request-ID) across service boundaries - Track key metrics: request rate, error rate, latency (p50, p95, p99), and saturation
- Set up alerts for error rate spikes (e.g., 5xx rate exceeds 1% over 5 minutes)
- Set up alerts for latency degradation (e.g., p99 exceeds 2 seconds)
- Monitor downstream dependency health (database, cache, third-party APIs)
- Implement health check endpoints (
/health,/ready) for load balancers and orchestrators - Retain logs for a meaningful period (30-90 days minimum)
- Use distributed tracing (OpenTelemetry, Jaeger) for microservice architectures
- Build dashboards showing real-time API health (Grafana, Datadog, or equivalent)
Health Check Endpoint
// GET /health
{
"status": "healthy",
"version": "1.4.2",
"uptime": "14d 6h 32m",
"checks": {
"database": { "status": "healthy", "latency_ms": 3 },
"cache": { "status": "healthy", "latency_ms": 1 },
"external_payment_api": { "status": "degraded", "latency_ms": 1200 }
}
}
Structured Log Entry
{
"timestamp": "2026-03-17T14:23:01.042Z",
"level": "info",
"method": "GET",
"path": "/api/v1/users/42",
"status": 200,
"duration_ms": 47,
"request_id": "req_abc123def456",
"user_id": "user_99",
"ip": "203.0.113.42"
}
A health check endpoint is not optional. Without it, your load balancer has no way to route traffic away from an unhealthy instance, and your orchestrator cannot restart a failing container. Keep the health check fast (under 100ms) and avoid heavy computation in it.
For a holistic view of your applicationβs performance beyond the API layer, the Frontend Performance Audit Checklist covers the client side of the equation.
11. Security Hardening
Security is not a single checkbox β it is a layered defense across every part of your API stack. These hardening measures protect against the most common attack vectors.
HTTPS & Transport Security
- Enforce HTTPS on all endpoints β no exceptions
- Enable HSTS (
Strict-Transport-Security: max-age=31536000; includeSubDomains) - Use TLS 1.2 or higher; disable older protocols
- Configure strong cipher suites and regularly audit them
CORS (Cross-Origin Resource Sharing)
- Restrict
Access-Control-Allow-Originto your known frontend domains β never use*in production with credentials - Explicitly list allowed methods and headers
- Set a reasonable
Access-Control-Max-Ageto reduce preflight requests - Never reflect the
Originheader back without validation
Security Headers
-
X-Content-Type-Options: nosniffβ prevent MIME type sniffing -
X-Frame-Options: DENYβ prevent clickjacking -
Content-Security-Policyβ restrict resource loading -
X-Request-IDβ attach a unique ID to every response for tracing - Remove server version headers (
Server,X-Powered-By) to avoid fingerprinting
Input & Data Protection
- Sanitize all user input against injection attacks (SQL, NoSQL, LDAP, OS command)
- Parameterize all database queries β never concatenate user input into SQL strings
- Encrypt sensitive data at rest (PII, tokens, credentials)
- Mask sensitive fields in logs (passwords, tokens, credit card numbers)
- Implement request size limits to prevent denial-of-service via large payloads
- Validate
Content-Typeheaders and reject unexpected types
Example: Secure Response Headers
curl -I https://api.example.com/api/v1/users
HTTP/2 200
strict-transport-security: max-age=31536000; includeSubDomains
x-content-type-options: nosniff
x-frame-options: DENY
content-security-policy: default-src 'none'; frame-ancestors 'none'
x-request-id: req_abc123def456
cache-control: no-store
content-type: application/json; charset=utf-8
A quick but often forgotten item: disable HTTP method override headers (X-HTTP-Method-Override) unless you explicitly need them. Attackers use these to bypass access controls by turning a GET into a DELETE.
12. Testing Strategy
An API without tests is a liability. A comprehensive testing strategy catches regressions before they reach production and gives your team confidence to ship fast.
Checklist
- Write unit tests for all business logic and validation rules
- Write integration tests for every endpoint (happy path and error cases)
- Test authentication flows: valid token, expired token, missing token, wrong scope
- Test authorization: ensure users cannot access resources they do not own
- Test pagination boundaries: first page, last page, empty results, oversized page
- Test rate limiting: verify 429 responses and Retry-After headers
- Test input validation: missing fields, wrong types, boundary values, overlong strings
- Test concurrent requests for race conditions (e.g., double-submit prevention)
- Run contract tests if your API is consumed by other teams or services
- Include load tests to verify performance under expected and peak traffic
- Automate tests in CI/CD β block merges on test failure
- Test backward compatibility when modifying existing endpoints
Example: Integration Test with curl
# Test: Creating a user returns 201 and includes Location header
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
https://api.example.com/api/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Test User", "email": "[email protected]", "role": "viewer"}')
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" != "201" ]; then
echo "FAIL: Expected 201, got $HTTP_CODE"
echo "$BODY"
exit 1
fi
echo "PASS: User created successfully"
echo "$BODY" | jq .
# Test: Accessing another user's resource returns 403
curl -s -o /dev/null -w "%{http_code}" -X GET \
https://api.example.com/api/v1/users/99/orders \
-H "Authorization: Bearer $USER_42_TOKEN"
# Expected: 403
Example: Load Test Scenario
# Using hey (https://github.com/rakyll/hey) for quick load testing
# 200 requests, 20 concurrent, targeting the list endpoint
hey -n 200 -c 20 \
-H "Authorization: Bearer $TOKEN" \
https://api.example.com/api/v1/products
# Check: p99 latency should be under 500ms
# Check: Zero 5xx errors
# Check: Rate limiting kicks in appropriately
Automated tests are not a luxury β they are the safety net that lets you deploy on Friday afternoon without breaking a sweat. Invest in them early, and they compound in value with every release.
Quick Reference Checklist
Below is a condensed summary you can print, pin to your wall, or paste into your teamβs project tracker. Each item maps to the detailed section above.
| # | Category | Key Items |
|---|---|---|
| 1 | URL Design | Plural nouns, lowercase hyphens, max 2 nesting levels, query params for filtering |
| 2 | HTTP Methods & Status Codes | Correct verbs (GET/POST/PUT/PATCH/DELETE), accurate status codes (201, 204, 400, 401, 403, 404, 409, 422, 429) |
| 3 | Authentication | HTTPS enforced, short-lived JWTs, scope-based authorization, brute-force protection |
| 4 | Validation | Schema validation library, reject unexpected fields, max body size, all errors returned at once |
| 5 | Error Handling | Consistent error envelope, machine-readable codes, request ID, no stack traces in production |
| 6 | Pagination | Cursor-based for large sets, default and max page size, pagination metadata in response |
| 7 | Rate Limiting | Standard headers, 429 with Retry-After, tiered limits, stricter on auth endpoints |
| 8 | Versioning | URL path versioning (v1), deprecation headers, 6-12 month sunset policy |
| 9 | Documentation | OpenAPI 3.x spec, interactive docs, realistic examples, changelog |
| 10 | Monitoring | Structured logs, request IDs, p50/p95/p99 latency, error rate alerts, health checks |
| 11 | Security | HSTS, CORS allowlist, security headers, input sanitization, sensitive data encryption |
| 12 | Testing | Integration tests per endpoint, auth edge cases, load tests, CI/CD gating |
Wrapping Up
Building a production-ready API is not about perfection on day one β it is about systematically covering the areas that cause the most pain when neglected. Authentication gaps lead to data breaches. Missing validation leads to corrupted data. Absent monitoring leads to blind spots that turn small issues into full outages.
Work through this checklist incrementally. Start with the non-negotiables: HTTPS, authentication, validation, and error handling. Then layer on rate limiting, versioning, and observability. Finally, invest in documentation and testing to make your API a pleasure to consume and maintain.
Every item you check off reduces your operational risk and increases developer trust in your API. That trust is what turns a simple backend service into a platform that teams and partners build on with confidence.
Ready to put this checklist into practice? Start by testing your existing endpoints with our API Request Builder, grab the API Testing Cheatsheet Pack for ready-made test templates, or scaffold your next project with the Fullstack Boilerplate Collection that has authentication, validation, and error handling baked in from the start.
Free Newsletter
Level Up Your Dev Workflow
Get new tools, guides, and productivity tips delivered to your inbox.
Plus: grab the free Developer Productivity Checklist when you subscribe.
Found this guide useful? Check out our free developer tools.