NodeJSREST API Best Practices

REST API Best Practices

This page consolidates everything from the REST section into a practical checklist — the conventions that separate an API people enjoy consuming from one they fight. None of it is new; it's the disciplined application of design, status codes, validation, errors, pagination, versioning, and security covered across this section.

Design & naming
  • Resource-based, plural nouns: /users, /orders/9/items — never verbs in the path.

  • Use HTTP methods for actions; respect their safe/idempotent semantics.

  • Lowercase, hyphenated paths; no file extensions; consistent trailing-slash policy.

  • Cap nesting at one level; filter/sort/paginate via the query string.

  • Version from day one (/v1/...) and only bump on breaking changes.

Status codes & responses

Situation

Status

Rule

Created a resource

201

  • Location header + the resource

Successful read/update

200

Return the resource

Successful delete

204

Empty body

Validation failed

422

Field-level details

Not authenticated / not allowed

401 / 403

Don't conflate them

Server fault

500

Generic message; log internally

The status code is the contract — never `200` an error
Caches, retries, client libraries, and monitoring all key off the HTTP status. Returning `200` with `{ success: false }` defeats every one of them. Match the code to the outcome (`4xx` client, `5xx` server), use a single [error envelope](/nodejs/api-error-responses), and never leak stack traces or internals on `5xx`.
Security essentials
  • Validate every input — body, params, query, headers; whitelist fields to stop mass assignment.

  • Authenticate then authorize — both, in that order, before handlers.

  • HTTPS only in production; helmet() for security headers.

  • Rate limit sensitive and expensive endpoints.

  • CORS whitelisted to known origins; never * with credentials.

  • Cap body size (limit) and upload size; reject oversized payloads with 413.

  • Never trust the client — re-validate server-side; client checks are UX only.

Security is not a feature you add later
Retrofitting validation, auth, and rate limiting onto a live API is far harder — and riskier — than building them in from the start. A single unvalidated endpoint or an over-permissive CORS policy can compromise the whole system. Treat the items above as non-negotiable baseline, not optional polish.
Performance & scale
  • Paginate every list with a capped limit; cursor pagination for large/live data.

  • Set Cache-Control on cacheable GETs; no-store on private data.

  • Index the columns you filter, sort, and paginate on.

  • Stay stateless so you can run many instances behind a load balancer.

  • Use a shared store (Redis) for rate limits and sessions across instances.

  • Compress responses once — in Node or the proxy, not both.

Reliability & operations
  • Centralize errors in one error-handling middleware; log 5xx fully.

  • Make idempotent operations truly idempotent; add idempotency keys for critical POSTs.

  • Add a health-check endpoint (GET /health) for load balancers.

  • Attach a request id to every request and log it for tracing.

  • Document with OpenAPI, generated from code where possible.

Consistency beats cleverness
The single most valuable property of an API is **consistency**: the same naming, the same error shape, the same pagination envelope, the same auth scheme — everywhere. A predictable API that's slightly imperfect is far easier to consume than a clever one with surprises on every endpoint. When in doubt, do what you did last time.
A reference structure to copy

The shape of a production Express API

JS
const app = express()

// ── Security & infrastructure (first) ──
app.use(helmet())
app.use(cors({ origin: allowedOrigins, credentials: true }))
app.use(compression())
app.use(morgan('combined'))
app.use(requestId())

// ── Parsing & limits ──
app.use(express.json({ limit: '100kb' }))
app.use(cookieParser())

// ── Rate limiting ──
app.use('/api', rateLimit({ windowMs: 60_000, max: 100 }))

// ── Versioned routes ──
app.use('/v1', v1Router)

// ── 404, then the central error handler (last) ──
app.use(notFoundHandler)
app.use(errorHandler)
This ordering is the sum of the whole section
Security headers and CORS first (they must cover everything), logging and request ids early, body parsing before routes, rate limiting in front of expensive work, versioned routes, then the 404 catch-all and the error handler **last**. Every position here is justified by a rule from [middleware ordering](/nodejs/application-middleware) — the structure *is* the best practices, made concrete.
Next
With REST mastered, render server-side HTML for traditional web apps: [Server-Side Templating](/nodejs/templating-intro).