API Design and Contracts (OpenAPI/Swagger)
API Design and Contracts (OpenAPI/Swagger)
Overview
An API contract describes requests and responses. A good contract reduces miscommunication and accelerates development.
Key practices
- Consistent naming (nouns for resources)
- Use proper status codes and error shapes
- Pagination and filtering conventions
- Idempotency for PUT/PATCH
Tools
- OpenAPI 3.0/3.1 YAML
- Swagger UI / Redoc
- Codegen for clients/servers
Example error shape
{
"error": {
"code": "VALIDATION_ERROR",
"message": "title is required",
"fields": { "title": "required" }
}
}
Checklist
- Write OpenAPI spec for core endpoints
- Share mock servers or example responses
- Add contract tests
API Design and Contracts (OpenAPI / Swagger)
Designing an API is like designing a user interface—only the users are other developers (your teammates, other services, external partners). A clear contract lowers coordination cost, prevents subtle breaking changes, and speeds up parallel work across frontend, backend, and QA.
1. Core Concepts
API Contract: A formal, versioned description of the surface area: endpoints, methods, request parameters, authentication requirements, and response schemas (including errors).
Schema‑First (Design‑First): Write or evolve the OpenAPI spec before/alongside implementation so other teams can stub clients or mock servers early.
Backward Compatibility: Avoid breaking existing clients—prefer additive changes (new fields, new endpoints) over mutation.
Mental model: Treat your API like a public library function. If you silently change the shape or meaning of an existing field, you impose hidden upgrade costs on every caller.
2. Resource Modeling
Pick clear nouns and stable identifiers. Map real world/business concepts, not UI labels.
| Anti-Pattern | Better | Reason |
|---|---|---|
/doPayment |
/payments (POST) |
Use nouns + HTTP verbs instead of RPC-ish verbs |
/user/list |
/users (GET) |
Collections are plural nouns |
/createProject |
/projects (POST) |
Consistency aids discoverability |
/orders/getActive |
/orders?status=active |
Filtering via query parameters |
Prefer hierarchical nesting only when the subordinate resource has no meaning outside the parent (e.g., /projects/{id}/members). Do NOT nest deeply (over 2 levels) – it hurts caching and clarity.
3. HTTP Methods & Idempotency
| Method | Typical Use | Idempotent? | Notes |
|---|---|---|---|
| GET | Retrieve resource(s) | Yes | Must not alter state |
| POST | Create / non-idempotent action | No | Returns 201 + Location header on create |
| PUT | Full replace (sends whole resource) | Yes | Client must send complete representation |
| PATCH | Partial update | Varies (should be) | Use JSON Patch or merge semantics; document behavior |
| DELETE | Remove resource | Yes | 204 on success; deletes should be safe to retry |
Idempotency helps clients retry safely during network hiccups. For POST endpoints creating external effects (payments, emails), consider an Idempotency-Key header.
4. Status Codes & Error Design
Use the small, predictable subset below. Avoid 200 for failures just because they “worked” technically.
| Situation | Code | Example |
|---|---|---|
| Created new resource | 201 | POST /projects |
| Empty success (no body) | 204 | DELETE /projects/{id} |
| Client sent invalid JSON/body | 400 | Malformed request |
| Unauthorized (no/invalid auth) | 401 | Missing token |
| Authenticated but not allowed | 403 | Role lacks permission |
| Resource not found | 404 | Unknown ID |
| Conflict / version mismatch | 409 | Duplicate key, optimistic lock fail |
| Too many requests | 429 | Rate limit exceeded |
| Service bug/downstream failure | 500 | Unexpected exception |
| Temporary upstream failure | 502/503 | Dependency outage |
Error Envelope Pattern
Keep errors structured and machine-friendly while still readable.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "title is required",
"fields": { "title": "required" },
"request_id": "9f3d6d2a"
}
}
request_id (trace id) so support/observability tools can correlate logs.
5. Pagination, Filtering, Sorting
Document defaults and maximums explicitly.
| Pattern | Pros | Cons |
|---|---|---|
limit + offset |
Simple, SQL friendly | Large offsets slow; duplicates after inserts |
| Cursor (opaque token) | Stable ordering across mutations | Slightly harder for newbies to debug |
Prefer cursor-based pagination for high-churn datasets (activity feeds) and offset for small, static lists (admin tables). Return a structure like:
{
"data": [ {"id":1}, {"id":2} ],
"next_cursor": "eyJpZCI6Mn0=",
"total": 42
}
total is optional (can be expensive). If omitted, document it.
Filtering: support simple equality ?status=active first; layer additional filters only as real use cases appear.
Sorting: ?sort=-created_at (dash for descending) is a common concise pattern.
6. Versioning Strategy
Avoid versioning too early; once you NEED it, pick one style and stick to it.
| Style | Example | Comments |
|---|---|---|
| URI | /v1/projects |
Easiest; leaks version into bookmarks |
| Header | Accept: application/vnd.api+json;version=1 |
Cleaner URIs; harder to demo in browser |
| Resource evolution | Additive only, deprecate fields | Works until a breaking semantic shift is needed |
Deprecate before removing: mark a field as deprecated in the schema + changelog + logs when clients still request it.
7. OpenAPI (Design‑First Workflow)
- Sketch resource list & main use cases (whiteboard or doc).
- Draft
/openapi.yamlwith core paths + schemas. - Share via Swagger UI or Stoplight; gather feedback BEFORE coding.
- Generate server stubs / client SDKs.
- Implement handlers; add contract tests guaranteeing spec ↔ implementation alignment.
- CI step: validate spec (lint) + diff detection (fail if breaking change).
Minimal YAML Example
openapi: 3.0.3
info:
title: Project Service API
version: 1.0.0
paths:
/projects:
post:
summary: Create a project
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateProject'
responses:
'201':
description: Created
headers:
Location:
schema:
type: string
'400': { $ref: '#/components/responses/BadRequest' }
components:
schemas:
CreateProject:
type: object
required: [name]
properties:
name: { type: string, maxLength: 80 }
description: { type: string }
responses:
BadRequest:
description: Invalid user input
8. Consistency Conventions
| Concern | Convention | Example |
|---|---|---|
| Field naming | snake_case or camelCase (pick one) | created_at |
| Timestamps | ISO 8601 UTC | 2025-01-12T15:04:05Z |
| Booleans | Avoid tri-state unless needed | is_active |
| Enums | Upper snake or lower kebab (document) | status: active|archived |
| IDs | UUID v4 or numeric; stay consistent | "a8f9..." |
| Null vs empty | Prefer empty list over null array | [] not null |
Document these in a short "API style guide" page if your team grows.
9. Security & Auth Touchpoints
Indicate security schemes in OpenAPI (components.securitySchemes). Common patterns:
* Bearer JWT: Authorization: Bearer <token>
* API Key (less preferred): X-API-Key: <key>
* OAuth2 flows (authorizationCode with PKCE for SPAs)
Also document rate limits: headers like X-RateLimit-Remaining help clients back off gracefully.
10. Testing the Contract
| Layer | Purpose | Tooling |
|---|---|---|
| Schema lint | Catch style + structural issues | Spectral, Redocly CLI |
| Mock server | Parallel frontend dev | Prism, Stoplight |
| Contract tests | Ensure implementation matches spec | Dredd, Schemathesis |
| Negative tests | Validate errors defined | Custom test harness |
Automate: a CI job fails if spec changed with a breaking diff (e.g., removed field). Keep a historical artifact (e.g., openapi.previous.json).
11. Example End-to-End Flow (Mermaid)
sequenceDiagram
participant FE as Frontend
participant SPEC as OpenAPI Spec
participant BE as Backend Service
participant QA as Contract Tests
FE->>SPEC: Read schema & generate client
BE->>SPEC: Update path /projects
SPEC-->>FE: New field `description` (non-breaking)
QA->>BE: Run tests against spec
BE-->>QA: 201 Created responses match
FE->>BE: POST /projects JSON
BE-->>FE: 201 + Location header
12. Checklist
- Core resources named with nouns & consistent plurality
- HTTP methods & status codes documented
- Error envelope standardized (code/message/request_id)
- Pagination & filtering strategy documented
- Versioning approach chosen (or explicitly deferred)
- OpenAPI spec validated & linted in CI
- Breaking-change diff guard enabled
- Auth schemes + rate limits defined
- Contract/negative tests implemented
- Changelog entries for schema additions
13. Quick Reference (Cheat Table)
| Need | Where to Look |
|---|---|
| Field types & validation | components.schemas.* |
| Auth method | components.securitySchemes + security |
| Error shapes | Shared #/components/responses |
| Pagination convention | Listing endpoints description |
| Rate limits | Usage docs / headers section |
14. Further Resources
- https://www.openapis.org/
- Swagger Editor – https://editor.swagger.io/
- Redoc / Redocly
- Stoplight Studio
- "API Design Patterns" (Book) – for deeper pragmatics
- Spectral Linter – enforce style