Skip to main content

✨ 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.
mindmap root((Great API)) Consistency Naming Response shape Errors Documentation OpenAPI Examples Robustness Versioning Backward compatibility Security AuthN & AuthZ Rate limiting Performance Caching Pagination

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 /invoice or /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 limit here, it isn't size there.

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

CodeUse when…
400 Bad RequestThe request is malformed (bad JSON, bad params)
401 UnauthorizedAuthentication is missing or invalid
403 ForbiddenAuthenticated, but not permitted
404 Not FoundThe resource doesn't exist
409 ConflictClashes with current state (duplicate, concurrent edit)
422 Unprocessable EntityWell-formed but fails business/validation rules
429 Too Many RequestsRate limit hit β€” include Retry-After
500 Internal Server ErrorAn 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:

StrategyExamplePros / Cons
URI path/api/v2/productsExplicit, easy to test in a browser β€’ same resource lives at two URLs
Query param/products?version=2Keeps the path clean β€’ easy to forget, complicates caching
Custom headerAccept-Version: 2Clean URIs β€’ invisible, harder to debug
Media typeAccept: application/vnd.example.v2+jsonMost 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.

flowchart LR A[Change to API] --> B{Breaking?} B -->|"Add endpoint / optional field"| C[No new version] B -->|"Remove or rename field
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:

Interactive API documentation layout A sidebar of endpoints on the left, and on the right a header, expandable endpoint rows, and a try-it panel. Endpoints E-commerce API β€” v1 GET /products POST /products Try it out category: [ electronics ] page: [ 1 ] Execute
Figure 1 β€” Interactive docs (Swagger UI / Redoc) generated from an OpenAPI file: a browsable endpoint list plus a live "try it out" panel.

βœ… 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:

MethodBest forWatch out for
API keys
X-API-Key: …
Server-to-server, simple public APIsNo expiry, all-or-nothing access β€” send in a header, never the URL
JWT
Authorization: Bearer …
SPAs, mobile apps, microservicesCan't be revoked before expiry β€” keep lifetimes short
OAuth 2.0Third-party / delegated accessMost 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 429 and Retry-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.

sequenceDiagram participant C as Client participant S as Server C->>S: GET /api/products/42 S-->>C: 200 OK Β· ETag "abc123" Β· {product} Note over C: caches body + ETag C->>S: GET /api/products/42 Β· If-None-Match "abc123" alt unchanged S-->>C: 304 Not Modified (empty) else changed S-->>C: 200 OK Β· ETag "def456" Β· {new product} end

βœ… 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; add Vary: Accept, Accept-Language when 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 Accepted with 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

  1. List at least five specific problems using the practices from this lesson and the previous one.
  2. Redesign every endpoint with the correct method, URL, and status code.
  3. 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

πŸš€ 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.