β¨ API Design Best Practices
A correct API and a great API are different things. This lesson covers the polish that separates the two: consistency, clear errors, sane versioning, first-class documentation, appropriate auth, and caching that keeps things fast. Your API is a contract β these practices make it one developers enjoy honoring.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Apply consistent naming and response-envelope conventions across an API
- Design structured, actionable error responses with the right status codes
- Choose and justify a versioning strategy and know what counts as a breaking change
- Describe an API with OpenAPI and explain why documentation drives adoption
- Pick an appropriate authentication method and apply core security and caching practices
Estimated Time: 40β50 minutes β’ Difficulty: Intermediate
Hands-on: Critique and redesign a deliberately bad API.
In This Lesson
The API as a Contract
An API is a promise your backend makes to everyone who consumes it β your own frontend, mobile apps, partners, and third parties. Once someone builds against it, that promise is expensive to break. Best practices are, at heart, ways to make the contract clear, consistent, and durable.
π‘ The restaurant-menu analogy. A great API is like a great menu: organized by category, with clear descriptions, consistent formatting, highlighted specials (common use cases), options for dietary needs (pagination, filtering), and updates that don't confuse the regulars (versioning). A confusing menu generates questions for the waitstaff; a confusing API generates support tickets.
Naming & Consistency
Consistency is the highest-leverage practice: if a developer learns one corner of your API, they should be able to guess the rest. Pick conventions and never deviate.
β Conventions to lock in
- Plural, concrete nouns for collections:
/invoices, not/invoiceor/items. - kebab-case in URLs (
/shipping-addresses), camelCase in JSON ({ "firstName": "..." }) β and be uniform. - ISO 8601 timestamps in UTC:
"createdAt": "2026-07-31T14:30:00Z". - One name per concept: if pagination uses
limithere, it isn'tsizethere.
A consistent response envelope
Wrapping payloads in a predictable envelope lets clients handle every response the same way. Collections carry pagination; every response can carry metadata:
{
"data": [
{ "id": 42, "name": "Wireless Headphones", "price": 129.99 }
],
"pagination": { "total": 100, "page": 2, "limit": 20 },
"meta": { "requestId": "abc123", "responseTime": "0.085s" }
}
π To envelope or not?
Envelopes aren't mandatory β some APIs (and JSON:API) structure things differently, and a bare array is valid. What matters is that you choose one shape and use it everywhere. The requestId in meta is especially valuable: return it in a header too, and support tickets become instantly traceable in your logs.
Error Handling
Errors are part of your API's UX. A good error response uses the right status code, gives a stable machine-readable code, a human-readable message, and β where helpful β the specific field at fault and a link to docs.
A single error
{
"error": {
"code": "INVALID_PARAMETER",
"message": "The parameter 'email' is not a valid email address.",
"details": { "parameter": "email", "value": "not-an-email" },
"documentationUrl": "https://api.example.com/docs/errors#INVALID_PARAMETER"
}
}
Multiple validation errors at once
Don't make clients fix one field, resubmit, and discover the next. Return every problem together with 422 Unprocessable Entity:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "The request contains 2 validation errors.",
"validationErrors": [
{ "field": "email", "code": "INVALID_FORMAT", "message": "Invalid email format." },
{ "field": "password", "code": "TOO_SHORT", "message": "Must be at least 8 characters." }
]
}
}
Status codes at a glance
| Code | Use when⦠|
|---|---|
| 400 Bad Request | The request is malformed (bad JSON, bad params) |
| 401 Unauthorized | Authentication is missing or invalid |
| 403 Forbidden | Authenticated, but not permitted |
| 404 Not Found | The resource doesn't exist |
| 409 Conflict | Clashes with current state (duplicate, concurrent edit) |
| 422 Unprocessable Entity | Well-formed but fails business/validation rules |
| 429 Too Many Requests | Rate limit hit β include Retry-After |
| 500 Internal Server Error | An unhandled failure on your side |
β οΈ Never leak internals
A 500 must not return a stack trace, SQL, or file paths β that's a gift to attackers. Log the gory details server-side against the requestId, and return a generic message the client can safely display.
Versioning Strategies
APIs change. Versioning lets them evolve without breaking existing clients. Four strategies dominate, each with trade-offs:
| Strategy | Example | Pros / Cons |
|---|---|---|
| URI path | /api/v2/products | Explicit, easy to test in a browser β’ same resource lives at two URLs |
| Query param | /products?version=2 | Keeps the path clean β’ easy to forget, complicates caching |
| Custom header | Accept-Version: 2 | Clean URIs β’ invisible, harder to debug |
| Media type | Accept: application/vnd.example.v2+json | Most REST-pure β’ most complex to use |
URI path versioning is the pragmatic default for most teams β it's the most visible and the easiest for consumers to adopt. Whichever you pick, be consistent and only bump the version for breaking changes.
change a type / required field"| D[New major version]
π‘ Deprecate gracefully
Run the old and new versions side by side, announce a timeline, and signal end-of-life with headers (e.g. Deprecation: true and a Sunset date). Give consumers a documented migration path β never yank a version out from under them.
Documentation & OpenAPI
Even a beautifully designed API sees poor adoption if developers can't figure out how to use it. Documentation isn't an afterthought β it's a feature.
The OpenAPI Specification (formerly Swagger) is the industry standard for describing HTTP APIs in a machine-readable format. From one OpenAPI file you can auto-generate interactive docs, client SDKs, server stubs, and request validators β and keep them all in sync with the code.
openapi: 3.1.0
info:
title: E-commerce API
version: 1.0.0
paths:
/products:
get:
summary: List all products
parameters:
- name: category
in: query
schema: { type: string }
- name: page
in: query
schema: { type: integer, default: 1 }
responses:
'200':
description: A paginated list of products
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: { $ref: '#/components/schemas/Product' }
components:
schemas:
Product:
type: object
properties:
id: { type: integer }
name: { type: string }
price: { type: number, format: float }
Tools like Swagger UI and Redoc render that spec into browsable, try-it-in-the-browser documentation. Below is the shape of a typical interactive docs page:
β Documentation checklist
- A getting-started guide with auth setup and a first working call.
- Complete request/response examples β including error cases, not just the happy path.
- Every error code listed with its cause and fix.
- Docs generated from (or checked against) the code so they never drift.
Authentication & Security
Pick the authentication method that fits your clients and threat model. The three you'll meet most often:
| Method | Best for | Watch out for |
|---|---|---|
API keysX-API-Key: β¦ | Server-to-server, simple public APIs | No expiry, all-or-nothing access β send in a header, never the URL |
JWTAuthorization: Bearer β¦ | SPAs, mobile apps, microservices | Can't be revoked before expiry β keep lifetimes short |
| OAuth 2.0 | Third-party / delegated access | Most complex; use a vetted library, don't roll your own |
A JWT-authenticated request looks like this:
GET /api/products HTTP/1.1
Host: shop.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
β οΈ Non-negotiable security practices
- HTTPS everywhere β never accept credentials over plain HTTP; add HSTS.
- Rate-limit to blunt brute-force and abuse; respond with
429andRetry-After. - Validate every input and use parameterized queries to stop injection.
- Least privilege β scope tokens to only what each client needs.
- Lock down CORS to trusted origins; don't pair
Access-Control-Allow-Origin: *with credentials.
Caching & Performance
The fastest response is the one you don't recompute. HTTP has caching built in β lean on it before reaching for exotic optimizations.
Conditional requests with ETags
The server tags a response with an ETag (a version fingerprint). The client sends it back on the next request via If-None-Match; if nothing changed, the server replies 304 Not Modified with an empty body β saving bandwidth and rendering work.
β Performance levers, cheapest first
- Compression β enable gzip/brotli; text payloads shrink dramatically for almost no effort.
- Cache-Control β mark cacheable reads:
Cache-Control: max-age=3600, must-revalidate; addVary: Accept, Accept-Languagewhen negotiating. - Pagination β never return an unbounded list; default and cap the
limit. - Field selection β let clients trim payloads:
/users/42?fields=id,name,email. - Server-side caching (e.g. Redis) for expensive queries, invalidated on write.
- Async processing β hand long jobs to a queue and return
202 Acceptedwith a status URL.
π‘ Measure before you optimize
Track response time (median and the 95th/99th percentiles), error rates, and per-endpoint volume. Include an X-Request-ID in responses so a client-reported problem maps straight to your logs. Optimize the slow endpoints the data actually points to β not the ones you guess about.
Hands-on Exercise
ποΈ Fix a bad API
Objective: Apply everything in this lesson by critiquing and redesigning a flawed design.
The original (broken) API
POST /createUser { "name":"John", "mail":"j@x.com", "passwd":"secret" }
GET /getUserInfo?id=123
POST /updateUser?id=123 { "name":"John Smith" }
GET /deleteUser?id=123
GET /getUsers?page=1&size=10&sortBy=name&sortDirection=ASC
Instructions
- List at least five specific problems using the practices from this lesson and the previous one.
- Redesign every endpoint with the correct method, URL, and status code.
- Write one full request + success response and one full error response in your redesign.
π‘ Hint
Look for: verbs in URLs, a destructive action behind GET (/deleteUser β a crawler could wipe your users!), inconsistent field names (mail, passwd), inconsistent pagination params (size vs limit), and no status codes or error structure.
β Example answer (excerpt)
Problems: verbs in URLs; DELETE done via GET (unsafe); mail/passwd should be email/password; size/sortBy/sortDirection are inconsistent; no status codes; no error format; password handling implied insecure.
POST /users # create -> 201 Created + Location
GET /users/123 # read -> 200 OK (404 if missing)
PATCH /users/123 # partial edit -> 200 OK
DELETE /users/123 # delete -> 204 No Content
GET /users?page=1&limit=10&sort=name&order=asc -> 200 OK
POST /users
{ "name": "John", "email": "j@x.com", "password": "s3cret!!" }
--> 201 Created
Location: /users/123
{ "data": { "id": 123, "name": "John", "email": "j@x.com" } }
--> 422 Unprocessable Entity
{ "error": { "code": "VALIDATION_FAILED",
"validationErrors": [
{ "field": "password", "code": "TOO_SHORT",
"message": "Must be at least 8 characters." } ] } }
π― Quick Quiz
Question 1: Which change to a published API is a breaking change that warrants a new version?
Question 2: A client re-requests a resource it already has, sending If-None-Match. Nothing changed. What should the server return?
Question 3: What is the main benefit of describing your API with an OpenAPI spec?
Summary & Quiz
π Key Takeaways
- Treat the API as a contract: consistency in naming, response shape, and errors is the highest-value practice.
- Return structured errors β status code + machine
code+ human message + offending field β and batch validation errors. - Version only for breaking changes; URI-path versioning is the pragmatic default, and deprecate old versions gracefully.
- OpenAPI turns one definition into docs, SDKs, and validators that stay in sync with the code.
- Choose auth to fit the client, enforce HTTPS + rate limiting + input validation, and lean on HTTP caching and pagination for speed.
π Further Reading
- The OpenAPI Initiative
- Google API Design Guide
- Microsoft REST API Guidelines
- Zalando RESTful API Guidelines
π What's Next?
You now know how to design a first-class API. Next we start building one: Node.js Runtime and Architecture introduces the event loop and non-blocking model that power the JavaScript backend you'll use to bring these designs to life.
π You've finished the API design track!
HTTP, REST, and best practices are in your toolkit. Time to write the server that serves them.