API Design Basics
8 examples to get you started with API Design for Node.js backends - 6 basic and 2 intermediate.
Prerequisites
Express 5 or Fastify 5 with JSON body parsing and Zod installed.
npm install express@5 zodBasic Examples
1. Resource-Oriented URLs
GET /v1/orders
POST /v1/orders
GET /v1/orders/:id
PATCH /v1/orders/:id
DELETE /v1/orders/:id- Nouns in paths; verbs come from HTTP methods
- Nested resources only one level deep:
/v1/orders/:id/items - Avoid
/createOrderRPC-style paths in public REST APIs
Related: Versioning & Deprecation -
/v1prefix
2. Consistent JSON Envelope
{ "data": { "id": "ord_1", "status": "pending" } }{ "error": { "code": "ORDER_NOT_FOUND", "message": "Order ord_99 not found" } }- Success responses wrap payload in
data - Errors use
errorobject with stablecodefor clients - Lists return
dataas array plus pagination meta
Related: Error Response Standards - Problem Details
3. Correct Status Codes
| Action | Code |
|---|---|
| Created resource | 201 |
| Success with body | 200 |
| Success no body | 204 |
| Validation failed | 400 |
| Missing auth | 401 |
| Forbidden | 403 |
| Not found | 404 |
| Conflict | 409 |
- Do not return
200with{ error: ... }in body 422acceptable for semantic validation if team standardizes it
4. Zod Validation at Boundary
import { z } from "zod";
import express from "express";
const createOrderBody = z.object({
customerId: z.string().uuid(),
sku: z.string().min(1),
qty: z.number().int().positive(),
});
const app = express();
app.use(express.json());
app.post("/v1/orders", (req, res) => {
const parsed = createOrderBody.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({
error: { code: "VALIDATION_ERROR", details: parsed.error.flatten() },
});
}
res.status(201).json({ data: { id: crypto.randomUUID(), ...parsed.data } });
});- Invalid JSON never reaches use cases
- Return field-level details for form clients
- Share schemas with frontend where possible
5. Idempotent PUT/PATCH Semantics
PATCH /v1/orders/ord_1
{ "status": "shipped" }PATCHpartial update; document allowed fieldsPUTfull replacement when you support it - rare in B2B APIs- Use
Idempotency-Keyheader onPOSTpayments (see intermediate)
6. Pagination Query Params Preview
GET /v1/orders?limit=20&cursor=eyJpZCI6Im9yZF8xMjMifQ- Prefer cursor pagination for large tables
- Offset acceptable for admin screens with small datasets
Related: Pagination & Filtering - cursor vs offset
Intermediate Examples
7. Content Negotiation and JSON Only
app.use((req, res, next) => {
if (req.method === "POST" && !req.is("application/json")) {
return res.status(415).json({ error: { code: "UNSUPPORTED_MEDIA_TYPE" } });
}
next();
});- Public APIs standardize on
application/json - Webhooks verify
Content-Typeand signature separately
8. OpenAPI Stub From Routes
Document routes as you ship them.
paths:
/v1/orders:
post:
summary: Create order
responses:
"201":
description: Created- Hand-written or code-generated OpenAPI prevents drift
Related: OpenAPI & Swagger - spec-first vs code-first
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.