Imported from CODE-SAURABH/OpenSkills (
api-design/SKILL.md). Install upstream withnpx skills add CODE-SAURABH/OpenSkills --skill api-design. Copyright stays with the author.
API Design
Approach every API as a product. The developer calling your API is your user. An API that is hard to understand, inconsistent, or unpredictable is a broken product — even if it technically works.
Design the API before writing a single line of implementation. An API is a contract. Changing it after clients depend on it is expensive. Getting it right upfront is cheap.
API Design Principles
- Spec first, implement second. Write the OpenAPI spec, GraphQL schema, or proto file before any implementation. The spec is the contract. Implementation details are irrelevant to the caller.
- Consistency is the most important quality. Every endpoint in an API should behave according to the same rules: same error format, same naming convention, same pagination pattern, same auth mechanism. Inconsistency forces callers to write special-case code for every endpoint.
- APIs are forever. Every field you add is a field you must support until you version or sunset the API. Every field you remove is a breaking change. Design with permanence in mind.
- Design for the caller, not the data model. Your database schema is an implementation detail. Your API shape should reflect what callers need, not what your ORM produces.
- Explicit over implicit. Undefined behaviour in an API becomes the behaviour callers depend on. Make every behaviour explicit: required vs optional fields, error codes, rate limits, pagination behaviour at the end of a list.
- Developer experience is a first-class requirement. An API that requires a PhD to understand will not be adopted. Good error messages, consistent naming, and accurate documentation are as important as correct behaviour.
Step 0: Ground the API Design
Before designing any endpoint, answer these questions:
- Who is the caller? Internal service, third-party developer, mobile app, browser, CLI? Each has different needs (auth mechanism, response size, error verbosity).
- What resources does the API expose? List the core domain entities the API manages.
- What operations does each resource support? CRUD is not always the right decomposition — sometimes
cancel,approve,publishare better verbs than generic CRUD. - What are the read/write patterns? High-read, low-write? Real-time? Bulk operations? Streaming? This determines the API style.
- What are the latency and payload size constraints? A mobile API on a 3G connection has different constraints than a backend-to-backend integration.
- What versioning strategy is needed from day one? If this API will have external consumers, versioning must be designed in before the first endpoint ships.
Choosing the Right API Style
| Style | Best for | Avoid when |
|---|---|---|
| REST | Resource-oriented APIs, public APIs, broad client compatibility, simple CRUD | Complex queries with many relationships, real-time, or highly variable response shapes |
| GraphQL | Flexible queries, multiple clients with different data needs, frontend-driven development, deeply nested data | Simple APIs, teams without GraphQL tooling, when over-fetching is not a real problem |
| gRPC | High-performance internal service communication, streaming, strongly-typed contracts, polyglot microservices | Browser clients (requires grpc-web proxy), teams unfamiliar with protobuf |
| WebSocket | Real-time bidirectional communication (chat, live dashboards, multiplayer) | Request-response patterns that do not need real-time; adds complexity without benefit |
| Webhooks | Asynchronous event notification to external systems | When the caller needs to poll or query state; use REST polling or SSE instead |
REST API Design Standards
URL Structure
# Pattern
/{version}/{resource}/{id}/{sub-resource}
# Examples — good
GET /v1/users
GET /v1/users/{userId}
GET /v1/users/{userId}/orders
POST /v1/users
PUT /v1/users/{userId}
PATCH /v1/users/{userId}
DELETE /v1/users/{userId}
# Actions that don't map to CRUD — use sub-resources
POST /v1/orders/{orderId}/cancel
POST /v1/invoices/{invoiceId}/send
POST /v1/users/{userId}/password-reset
URL rules:
- Always lowercase, hyphen-separated (
user-profiles, notuserProfilesoruser_profiles) - Nouns for resources, not verbs (
/users, not/getUsers) - Plural for collections (
/users, not/user) - Version in the URL path (
/v1/) for public APIs — query param or header for internal APIs - Never expose database IDs directly where possible (use UUIDs or opaque string IDs)
HTTP Methods — Correct Usage
| Method | Semantics | Idempotent | Safe |
|---|---|---|---|
GET |
Retrieve resource(s) | ✅ | ✅ |
POST |
Create a new resource or trigger an action | ❌ | ❌ |
PUT |
Replace a resource entirely | ✅ | ❌ |
PATCH |
Partially update a resource | ❌ (should be) | ❌ |
DELETE |
Remove a resource | ✅ | ❌ |
Rule: GET requests must never have side effects. Never use GET to trigger a state change.
Request & Response Shape
Request body — always:
{
"name": "Jane Doe",
"email": "jane@example.com",
"role": "admin"
}
Single resource response:
{
"id": "usr_01HXYZ",
"name": "Jane Doe",
"email": "jane@example.com",
"role": "admin",
"createdAt": "2026-01-15T10:30:00Z",
"updatedAt": "2026-01-15T10:30:00Z"
}
Collection response — always wrap in an envelope:
{
"data": [
{ "id": "usr_01HXYZ", "name": "Jane Doe" },
{ "id": "usr_02HABC", "name": "John Smith" }
],
"pagination": {
"cursor": "eyJpZCI6InVzcl8wMkhBQkMifQ==",
"hasMore": true,
"total": 247
}
}
Why an envelope? Adding metadata (pagination, request ID, warnings) to a bare array response is a breaking change. An envelope allows non-breaking additions forever.
Naming Conventions
- Field names:
camelCasefor JSON APIs (firstName, notfirst_nameorFirstName) - Timestamps: ISO 8601 UTC (
"2026-01-15T10:30:00Z") — never Unix timestamps in the response body - Booleans: positive framing (
isActive, notisNotActive;isEnabled, notisDisabled) - IDs: string type always, even if internally numeric (prevents JavaScript integer overflow)
- Money: integer cents, never floating-point (
"amount": 1999means $19.99) - Enums: SCREAMING_SNAKE_CASE (
"status": "IN_PROGRESS")
Error Response Standard
Every API must have one error format used consistently across all endpoints:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Must be a valid email address"
}
],
"requestId": "req_01HXYZ123",
"docsUrl": "https://docs.example.com/errors/VALIDATION_ERROR"
}
}
Rules:
codeis a machine-readable string constant — callers switch on this, not on the HTTP statusmessageis human-readable — never put a machine-parseable value heredetailsis an array — multiple validation errors in one response, never force callers to fix one error at a timerequestIdon every error response — this is how support traces the request in logsdocsUrlfor each error code — link to documentation explaining the error and how to fix it
HTTP Status Codes — Correct Usage
| Code | When to use |
|---|---|
200 OK |
Successful GET, PATCH, PUT |
201 Created |
Successful POST that creates a resource |
204 No Content |
Successful DELETE or action with no response body |
400 Bad Request |
Validation error, malformed request |
401 Unauthorized |
Not authenticated |
403 Forbidden |
Authenticated but not authorized for this resource |
404 Not Found |
Resource does not exist |
409 Conflict |
State conflict (duplicate, version mismatch) |
422 Unprocessable Entity |
Semantically invalid request (valid syntax, invalid business logic) |
429 Too Many Requests |
Rate limit exceeded |
500 Internal Server Error |
Unexpected server error |
503 Service Unavailable |
Planned downtime or dependency unavailable |
Never use 200 with an error body. { "success": false, "error": "..." } with a 200 status is a broken API.
Pagination Patterns
Cursor-based Pagination (recommended for most cases)
GET /v1/users?cursor=eyJpZCI6InVzcl8wMkhBQkMifQ==&limit=20
When: Ordered, append-heavy collections (feeds, logs, events). Stable pages even when new items are inserted.
Offset-based Pagination (simple, but has edge cases)
GET /v1/products?page=3&pageSize=20
When: Admin UIs where users jump to specific pages. Avoid for large or frequently-updated datasets (items shift as pages load).
Keyset Pagination (for high-performance sorted queries)
GET /v1/orders?afterId=ord_01HXYZ&limit=50
When: Database queries on an indexed column where offset queries become slow.
Pagination response fields (always include):
cursorornextPage— how to get the next pagehasMore(boolean) — whether more results exist after this pagetotal(optional) — total count (expensive on large datasets; omit if not needed)limit— the limit that was applied (echo it back)
API Versioning Strategy
URL Path Versioning (recommended for public APIs)
/v1/users
/v2/users
Pros: Explicit, easy to route in proxies/gateways, cacheable. Use for: External/public APIs, mobile app APIs (clients pin to a version).
Header Versioning (recommended for internal APIs)
API-Version: 2026-01-15
Pros: Keeps URLs clean; date-based versions are self-documenting. Use for: Internal services, APIs with sophisticated clients.
Breaking vs Non-Breaking Changes
Non-breaking (safe to add without versioning):
- Adding new optional fields to responses
- Adding new optional request parameters
- Adding new endpoints
- Loosening validation on existing fields
Breaking (requires version bump):
- Removing or renaming fields
- Changing field types
- Changing HTTP status codes
- Tightening validation
- Changing authentication requirements
- Removing endpoints
Deprecation policy: Mark deprecated fields with a X-Deprecated-Fields response header and a deprecated note in the OpenAPI spec. Give callers a minimum of 6 months notice before removal.
OpenAPI Spec Standards
Every REST API ships with an OpenAPI 3.x spec. No exceptions.
openapi: 3.1.0
info:
title: User Management API
version: 1.0.0
description: Manages user accounts and profiles
paths:
/v1/users/{userId}:
get:
summary: Get a user by ID
operationId: getUserById
tags: [Users]
parameters:
- name: userId
in: path
required: true
schema:
type: string
responses:
'200':
description: User found
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
$ref: '#/components/responses/NotFound'
'401':
$ref: '#/components/responses/Unauthorized'
components:
schemas:
User:
type: object
required: [id, name, email, createdAt]
properties:
id:
type: string
example: usr_01HXYZ
name:
type: string
example: Jane Doe
email:
type: string
format: email
createdAt:
type: string
format: date-time
OpenAPI rules:
operationIdon every endpoint — used for SDK generationtagson every endpoint — groups endpoints in documentation- Every response code documented — not just 200
$reffor shared schemas — no duplication- Realistic
examplevalues — notstring,foo, or123 - Security schemes defined and applied to every protected endpoint
Rate Limiting
Every public API must implement rate limiting. Communicate it clearly:
Response headers (always include on rate-limited APIs):
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 842
X-RateLimit-Reset: 1737892800
Retry-After: 60 (only on 429 responses)
Rate limit strategy:
- Per API key / per user: prevents one caller from starving others
- Per endpoint: expensive endpoints get tighter limits than cheap ones
- Sliding window preferred over fixed window (avoids burst at window boundary)
- Return
429withRetry-Afterheader — never silently drop requests
Authentication Standards
| Mechanism | Use case |
|---|---|
| Bearer JWT | User-facing APIs, mobile/web clients |
API Key (Authorization: Bearer sk_...) |
Server-to-server, third-party integrations |
| OAuth 2.0 + PKCE | Third-party access on behalf of a user |
| mTLS | High-security internal service communication |
Rules:
- Always use
Authorization: Bearer <token>header — never query parameters for auth tokens - API keys in query parameters appear in server logs. Never.
- Short-lived access tokens (15 min – 1 hour) with refresh token rotation
- Scopes on API keys — a key should only have access to what it needs
Bundled Reference
Read openapi-review.md when reviewing or writing an OpenAPI contract. It supplements, but does not replace, the API-specific requirements in this skill.
API Design Review Checklist
Before finalising any API design:
Resource & URL Design
- URLs are nouns, lowercase, hyphenated
- HTTP methods used correctly (no state-changing GETs)
- Version included in URL or header strategy defined
- IDs are string type (not integer)
- Actions not mappable to CRUD use sub-resource + POST
Request & Response
- Collections wrapped in an envelope object
- Consistent field naming (camelCase)
- Timestamps in ISO 8601 UTC format
- Money in integer cents, not floats
- All required fields documented
Errors
- Single error format used across all endpoints
- Machine-readable error codes defined
- Field-level validation errors returned in one response (not one at a time)
- Correct HTTP status codes used
- RequestId on every error response
Pagination
- All collection endpoints paginated (never return unbounded lists)
- Pagination strategy documented
-
hasMoreand cursor/nextPage in every paginated response
Versioning & Breaking Changes
- Versioning strategy defined before first endpoint ships
- Deprecation policy defined
- No breaking changes to existing endpoints without a version bump
Security
- Auth mechanism defined for all endpoints
- Rate limiting applied and communicated via headers
- No secrets in URLs or query parameters
- HTTPS only — no HTTP fallback
Documentation
- OpenAPI 3.x spec complete with all endpoints, schemas, and error responses
- Realistic examples on all request/response fields
- Authentication documented with example token format
- Rate limits documented