Index | Previous: Security and Permissions
Define your API contract before writing implementation code. This isn't bureaucracy; it's pragmatism.
Why contract-first:
- Parallel development - Frontend and backend teams can work simultaneously. Frontend mocks the API while backend implements it.
- Catch design flaws early - It's cheaper to change a spec than refactor code.
- Better APIs - When you design without implementation pressure, you make better decisions.
- Documentation is always current - The spec is the source of truth, not an afterthought.
How to do it:
- Write the OpenAPI1 spec first
- Review with consumers (frontend, mobile, partners)
- Generate server stubs and client SDKs using OpenAPI Generator2
- Implement against the contract
- Use contract testing3 to ensure implementation matches spec
# Design the contract first
openapi: 3.0.3
info:
title: Orders API
version: 1.0.0
paths:
/orders:
post:
summary: Create an order
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOrderRequest'
responses:
'201':
description: Order created
content:
application/json:
schema:
$ref: '#/components/schemas/Order'The alternative, code-first with generated docs, leads to APIs shaped by implementation convenience rather than consumer needs.
Your API should reflect your business domain, not your database schema or internal architecture. See Eric Evans' Domain-Driven Design4 and Martin Fowler's DDD summary5.
Ubiquitous language6 - Use the same terms your business uses. If the business calls it an "order," don't call it a "purchase transaction" in your API. If they say "cancel," don't say "terminate."
Bounded contexts7 - Large systems have multiple domains. An "order" in shipping means something different than an "order" in billing. Don't force a single unified model; let each context define its own.
Aggregates8 - Group related entities that change together. An order and its line items are an aggregate. Expose the aggregate root (/orders/{id}) not the internals (/line-items/{id}).
# Good - reflects domain concepts
POST /orders
POST /orders/{id}/cancel
GET /customers/{id}/orders
# Bad - exposes implementation
POST /order_records
PUT /orders/{id}/status (with body: { "status": "cancelled" })
GET /database/customers/join/orders
Anti-corruption layer9 - When integrating with legacy systems or third-party APIs, don't let their models pollute your domain. Translate at the boundary.
Treat your API as a product, not a by-product. Someone will consume it, and their experience matters. See API as a Product10 for more on this mindset.
- Consistency is kindness - Inconsistent APIs waste developers' time. If one endpoint uses
created_atand another usescreatedDate, someone will get it wrong. - Versioning is a promise - When you publish v1, you're promising not to break it. Take that seriously.
- Errors are documentation - A good error message teaches the developer what went wrong and how to fix it.
- Deprecation is communication - Don't just remove things. Warn, provide migration paths, give timelines.
Keep your services designed to serve resources. Otherwise you risk your services becoming remote procedure calls. REST11 is Representational State Transfer, not RPC.
The naming convention is very important because it implies consistency. The naming convention should not leak implementation details. It should relate to resources. See Google's API Design Guide12 for comprehensive naming guidance.
Endpoints should be nouns, such as books or users. Names that are verbs or adjectives are problematic:
| Bad | Good |
|---|---|
/doPayroll |
/payroll |
/createUser |
POST /users |
/getUserById |
GET /users/{id} |
Resources should be versioned. There are two main approaches:
URL-based versioning works best for API teams:
/v2/books
This makes it easy to stand up separate servers behind a load balancer without convoluting code with versioning concerns. Use only the major version number. Avoid /v2.14.2/books which becomes unmanageable.
Header-based versioning offers more flexibility:
Accept-Version: 2
This allows clients to upgrade independently but adds complexity to routing.
Refer to Semantic Versioning13 for version numbering. Use Sunset headers per RFC 859414 to communicate deprecation timelines.
When I found that I needed to support multiple versions, I usually added statistics on the endpoint to find when it was no longer being used, and removed it only after inactivity.
Resources should always have plural names:
GET /books
GET /books/1234
POST /books
Avoid singular names like /book. The plural form makes it clear that the endpoint represents a collection, and individual resources are accessed by ID.
Create, Read, Update, Delete should be represented through HTTP methods per RFC 911015:
| Operation | HTTP Verb | Example |
|---|---|---|
| Create | POST | POST /books |
| Read (list) | GET | GET /books |
| Read (single) | GET | GET /books/123 |
| Update (full) | PUT | PUT /books/123 |
| Update (partial) | PATCH | PATCH /books/123 |
| Delete | DELETE | DELETE /books/123 |
Self-discovery, or HATEOAS16 (Hypermedia as the Engine of Application State), means that links within the response enable discovery of related endpoints and actions. The idea is powerful: clients navigate the API by following links rather than constructing URLs, reducing coupling between client and server. See Roy Fielding's explanation17.
When including links, always include a rel value to describe the relationship. Use IANA link relations18 where applicable:
{
"id": "book_abc123",
"title": "API Design Patterns",
"links": [
{ "rel": "self", "href": "/books/book_abc123" },
{ "rel": "author", "href": "/authors/auth_xyz789" },
{ "rel": "reviews", "href": "/books/book_abc123/reviews" }
]
}Document all rel types in your API specification. This allows consumer developers to create global handlers rather than one-off designs for each endpoint.
HATEOAS is academically elegant but rarely implemented fully in practice. Here's why:
Implementation cost is high. Every response needs link generation logic. Links must be context-aware (different users see different available actions). Maintaining this across a large API is substantial work.
Clients rarely use it. Most API consumers want fit-for-purpose endpoints that return exactly what they need for a specific use case. They don't want to navigate a web of links to assemble data. Frontend developers typically hardcode URLs because it's simpler and more predictable.
Entity-centric vs use-case-centric. HATEOAS pushes you toward microscopic entity relationships: a book links to its author, the author links to their other books, those books link to their reviews. But real applications need "get me everything for the book detail page" not "let me traverse the entity graph." This is where REST's resource model and GraphQL's query model diverge.
State explosion. If links represent available actions based on state (an order can be cancelled only when it's pending), link generation becomes complex business logic. You're encoding state machines into every response.
- Public APIs with long-lived clients. If you can't update clients when URLs change, link discovery helps.
- Pagination. Including
next,prev, andlastlinks is genuinely useful. - Discoverable APIs. Developer portals and API explorers benefit from self-describing responses.
- Workflow APIs. When the API genuinely models a state machine (order processing, approval workflows), links showing available transitions add value.
Adopt HATEOAS selectively:
- Always include
selflinks for resources - Include pagination links for collections
- Include links for common related resources (author on a book)
- Skip the ideology of "clients should never construct URLs"
Most successful APIs use partial HATEOAS: links where they're useful, not religious adherence to the constraint.
Idempotent operations produce the same result regardless of how many times they're called. GET, PUT, and DELETE are naturally idempotent. POST is not. See RFC 9110 on idempotency19.
For non-idempotent operations, use an idempotency key. Stripe's implementation20 is a good reference:
POST /payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"amount": 1000,
"currency": "USD"
}The server should:
- Check if this key has been seen before
- If yes, return the cached response
- If no, process the request and cache the response
This prevents duplicate operations from network retries or client bugs. Keys should expire after a reasonable period (24-48 hours).
Use ETags per RFC 723221 and conditional headers to optimise caching and prevent update conflicts.
For caching (GET requests):
GET /books/123
If-None-Match: "abc123"Returns 304 Not Modified if unchanged, saving bandwidth.
For optimistic locking (PUT/PATCH requests):
PUT /books/123
If-Match: "abc123"
Content-Type: application/json
{ "title": "Updated Title" }Returns 412 Precondition Failed if the resource has changed since the client last fetched it, preventing lost updates. See MDN's guide on conditional requests22.
Cross-Origin Resource Sharing (CORS)23 is essential for browser-based API consumers. Configure these headers:
Access-Control-Allow-Origin: https://trusted-domain.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, Idempotency-Key
Access-Control-Max-Age: 86400Guidelines:
- Never use
Access-Control-Allow-Origin: *with credentials - Validate the
Originheader against an allowlist - Keep preflight cache (
Max-Age) high for performance - Only expose headers clients actually need
See MDN's CORS guide24 for detailed implementation guidance.
Provide health check endpoints for load balancers and orchestrators. See Kubernetes health check patterns25.
Liveness - Is the service running?
GET /health/live
200 OK
Readiness - Is the service ready to accept traffic?
GET /health/ready
{
"status": "healthy",
"checks": {
"database": "up",
"cache": "up",
"external_api": "degraded"
}
}Return 200 for healthy, 503 for unhealthy. Keep liveness checks simple (no dependencies). Readiness checks can verify database connections and critical dependencies.
Include correlation IDs for distributed tracing. See W3C Trace Context26 and OpenTelemetry27.
GET /orders/123
X-Request-ID: req_abc123
X-Correlation-ID: corr_xyz789- Request ID - Unique to this request, generated by the API if not provided
- Correlation ID - Passed through the entire call chain across services
Log these IDs in every service. This makes debugging distributed systems tractable.
URLs are visible in logs, browser history, and network monitoring tools. Never expose sensitive information in URL parameters or path segments. See OWASP's guidance on sensitive data28.
Path parameters - Use for resource identification:
GET /users/123
GET /users/123/orders/456
Query parameters - Use for filtering, sorting, and pagination:
GET /users?status=active&sort=-createdAt&limit=20
Request body - Use for sensitive data or complex payloads:
POST /auth/login
Content-Type: application/json
{ "email": "user@example.com", "password": "..." }If an entity has a unique ID, use only that ID in the path. Don't include redundant parent IDs even for nested resources unless required for authorization checks.
Choose a naming convention and apply it consistently. The most common options:
| Convention | Example | Common in |
|---|---|---|
| camelCase | firstName, createdAt |
JavaScript, Java |
| snake_case | first_name, created_at |
Python, Ruby, databases |
| kebab-case | first-name, created-at |
URLs, CSS (avoid in JSON) |
camelCase is widely adopted because:
- JavaScript (the dominant API consumer language) uses it natively
- JSON29 originated from JavaScript
- Most API documentation examples use it
Whatever you choose, be consistent across all endpoints and responses. Don't mix conventions within the same API. See Google's JSON style guide30.
Written by Philip A Senger | LinkedIn | GitHub
This work is licensed under a Creative Commons Attribution 4.0 International License.
Previous: Security and Permissions | Next: Resilience
Footnotes
-
OpenAPI Initiative. "OpenAPI Specification." https://www.openapis.org/ ↩
-
OpenAPI Generator. "Generate clients, servers, and documentation from OpenAPI specifications." https://openapi-generator.tech/ ↩
-
Pact Foundation. "Pact - Contract Testing." https://pact.io/ ↩
-
Evans, Eric. (2003). "Domain-Driven Design: Tackling Complexity in the Heart of Software." Addison-Wesley. https://www.domainlanguage.com/ddd/ ↩
-
Fowler, Martin. "Domain-Driven Design." https://martinfowler.com/bliki/DomainDrivenDesign.html ↩
-
Fowler, Martin. "Ubiquitous Language." https://martinfowler.com/bliki/UbiquitousLanguage.html ↩
-
Fowler, Martin. "Bounded Context." https://martinfowler.com/bliki/BoundedContext.html ↩
-
Fowler, Martin. "DDD Aggregate." https://martinfowler.com/bliki/DDD_Aggregate.html ↩
-
Microsoft. "Anti-Corruption Layer pattern." Azure Architecture Patterns. https://docs.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer ↩
-
APIScene. "API as a Product." https://www.apiscene.io/api-as-a-product/ ↩
-
Fielding, Roy Thomas. (2000). "Architectural Styles and the Design of Network-based Software Architectures." Doctoral dissertation, University of California, Irvine. Chapter 5: REST. https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm ↩
-
Google Cloud. "API Design Guide." https://cloud.google.com/apis/design ↩
-
Preston-Werner, Tom. "Semantic Versioning 2.0.0." https://semver.org/ ↩
-
Wilde, E. (2019). "The Sunset HTTP Header Field." RFC 8594, IETF. https://datatracker.ietf.org/doc/html/rfc8594 ↩
-
Fielding, R. et al. (2022). "HTTP Semantics." RFC 9110, IETF. Section 9: Methods. https://httpwg.org/specs/rfc9110.html#methods ↩
-
Wikipedia. "HATEOAS." https://en.wikipedia.org/wiki/HATEOAS ↩
-
Fielding, Roy. (2008). "REST APIs must be hypertext-driven." https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven ↩
-
IANA. "Link Relations." https://www.iana.org/assignments/link-relations/link-relations.xhtml ↩
-
Fielding, R. et al. (2022). "HTTP Semantics." RFC 9110, IETF. Section 9.2.2: Idempotent Methods. https://httpwg.org/specs/rfc9110.html#idempotent.methods ↩
-
Stripe. "Idempotent Requests." Stripe API Documentation. https://stripe.com/docs/api/idempotent_requests ↩
-
Fielding, R. and Reschke, J. (2014). "Hypertext Transfer Protocol (HTTP/1.1): Conditional Requests." RFC 7232, IETF. https://datatracker.ietf.org/doc/html/rfc7232 ↩
-
MDN Web Docs. "HTTP conditional requests." https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests ↩
-
WHATWG. "Fetch Standard - CORS Protocol." https://fetch.spec.whatwg.org/#http-cors-protocol ↩
-
MDN Web Docs. "Cross-Origin Resource Sharing (CORS)." https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS ↩
-
Kubernetes. "Configure Liveness, Readiness and Startup Probes." https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ ↩
-
W3C. "Trace Context." https://www.w3.org/TR/trace-context/ ↩
-
OpenTelemetry. "High-quality, ubiquitous, and portable telemetry." https://opentelemetry.io/ ↩
-
OWASP. "Sensitive Data Exposure." https://owasp.org/www-project-web-security-testing-guide/ ↩
-
Crockford, Douglas. "Introducing JSON." https://www.json.org/ ↩
-
Google. "Google JSON Style Guide." https://google.github.io/styleguide/jsoncstyleguide.xml ↩