How do you design and secure Node.js APIs end to end?
Node.js Developer
answer
I design Node.js APIs with schema-first input validation, strict authentication (JWT/OAuth2 with short-lived tokens and refresh rotation), and authorization via roles/attributes at the route/service layer. I prevent injection and client risks with output encoding, CSP, and CORS. CSRF is blocked via same-site, double-submit tokens, or proof-of-possession. I freeze prototypes, pin dependencies, sandbox unsafe parsing, and instrument rate limits, audit logs, and anomaly alerts.
Long Answer
A secure Node.js API starts with clear trust boundaries, then enforces correctness at every hop: inputs, identity, permissions, and outputs. I use a layered approach that treats validation and authorization as code, not convention.
1) Interface & validation (fail closed)
I adopt schema-first contracts (OpenAPI/JSON Schema). Every handler validates params, query, and body with Zod/Yup/Ajv before touching business code. Policies:
- Deny by default: unknown properties stripped; lengths, enums, formats enforced (email, UUID, ISO dates).
- Canonicalization: trim/normalize strings, lower-case emails, parse numbers safely; reject ambiguous encodings.
- File inputs: verify MIME/extension, scan size/virus, store outside webroot, generate random names.
- Mass assignment: whitelist assignment fields; never spread raw body into models.
2) Authentication (who are you?)
For first-party apps I prefer OAuth2/OIDC with Authorization Code + PKCE; for pure APIs I issue JWT access tokens scoped to resources. Practices:
- Short-lived access tokens (5–15m) + refresh rotation with reuse detection.
- Asymmetric signing (RS/ES) with kid rotation via JWKS; verify iss/aud/exp/nbf.
- Transport: TLS 1.2+, HSTS, secure cookie for session tokens; avoid localStorage for long-lived secrets.
- M2M: use Client Credentials with fine-grained scopes; never re-use human tokens.
3) Authorization (what can you do?)
I centralize authorization behind a policy layer:
- RBAC + ABAC: start with roles, extend with attributes (tenant, region, ownership).
- Resource scoping: verify tenant IDs on every query; never trust client-provided userId/tenantId—derive from token claims.
- Least privilege: require explicit scopes/permissions per route; deny read-modify-write without ownership checks.
- Query guards: inject filters server-side (e.g., WHERE tenant_id = :fromToken) to prevent horizontal escalation.
4) Threat coverage & hardening
XSS (for mixed API/HTML responses): escape output, set CSP (default-src 'self'), encode JSON safely ()]}', prefix when embedding). For pure JSON APIs, keep responses JSON-only with correct Content-Type.
CSRF: Prefer stateless Bearer in Authorization header (not cookies). If cookie-based, enforce SameSite=Lax/Strict, HttpOnly, CSRF tokens (double-submit) or DPoP/PoP for proof-of-possession.
Injection: use parameterized queries/ORM; never build SQL/NoSQL strings from input. Disable $where in Mongo; validate operators; sanitize regex.
Prototype pollution: deny __proto__, constructor, prototype keys; deep clone with safe libraries; use Object.create(null) for maps; freeze configs with Object.freeze.
Path traversal: resolve paths with path.resolve, ensure within allowed base dir, never echo raw file paths.
Command injection: prefer library APIs; if exec is unavoidable, escape args, use allow-lists, drop privileges.
Deserialization: avoid eval-like parsing; for YAML/JSON5 use safe modes; cap payload sizes to stop DoS.
5) Transport, CORS, and headers
- CORS: restrict origin, methods, and headers; avoid * with credentials; preflight cache controlled.
- Security headers: X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy, Strict-Transport-Security.
- Rate limit / Bot defense: token bucket per IP + user/tenant; exponential backoff; captcha only at abuse thresholds.
- DoS protection: body size limits, timeouts, circuit breakers for upstreams, queue/backpressure.
6) Secrets, keys, and configs
Manage secrets via environment injection (Vault/KMS/Secrets Manager). Rotate JWT signing keys with overlapping validity; publish JWKS. Use NODE_OPTIONS=--disable-proto=throw (supported runtimes) to harden prototypes. Enforce npm provenance: lockfiles, npm audit, npm pkg provenance, and allow-list registries.
7) Observability & audit
Structured logs (request ID, user/tenant, scope, outcome), privacy-safe (no tokens/PII). Emit security events (login, consent, role changes, failed auth, refresh reuse). Traces around external calls; metrics on p95 latency, error rates, 401/403 ratios, rate-limit triggers. Alert on anomalies (sudden spike of 401→200, mass 5xx).
8) Testing & CI/CD
- Unit: validators, policy functions, claim checks.
- Integration: happy-path and abuse-path suites (oversized payloads, traversal, proto pollution keys).
- Contract: OpenAPI validation in CI; Dredd/Prism mocks for consumers.
- Security: SAST/DAST, dependency scan, secret scan, license check.
- Deploy: immutable images, minimal base, non-root user, read-only FS, drop CAP_*.
9) Example flow (update own profile)
- Client requests token via OAuth2; receives access (10m) + rotating refresh.
- Calls PATCH /me with Bearer token; gateway verifies signature, iss/aud/exp; policy resolves subject.
- Input validated (allowed fields only).
- Authorization checks ownership (sub matches record).
- DB update via parameterized query.
- Response JSON with ETag; logs record the action, no PII; metrics count a success.
This blueprint keeps Node.js APIs correct by construction, minimizing exploit surfaces and ensuring identity and authorization are continuously verified.
Table
Common Mistakes
- Validating only request bodies, ignoring query/params/headers.
- Long-lived JWTs without refresh rotation or revocation; using HS256 with shared secrets across services.
- Trusting client-sent IDs for authorization instead of deriving from claims.
- Using cookies with SameSite=None but missing Secure; storing tokens in localStorage.
- Open CORS (*) with credentials enabled.
- Building SQL/NoSQL strings by concatenation; allowing $where/unbounded regex in Mongo.
- Accepting prototype keys (__proto__, constructor) during deep merges.
- No body size limits or timeouts; app crashes under large uploads or slowloris.
- Logging tokens/PII; missing audit trails for role changes/login failures.
- Unpinned dependencies, disabled npm audit, and running Node as root in production.
Sample Answers
Junior:
“I validate inputs with Zod and use parameterized queries. Auth uses JWT access tokens and I check iss/aud/exp. CORS is restricted to known origins. I send cookies with HttpOnly and SameSite, and add basic rate limiting.”
Mid:
“I design schema-first APIs (OpenAPI + Ajv), short-lived JWTs with RS256 and refresh rotation, and authorization via RBAC plus tenant filters derived from claims. I prevent CSRF with SameSite cookies and CSRF tokens when needed, and enforce CSP for any HTML. I block prototype keys and set strict CORS and headers.”
Senior:
“I implement OIDC with PKCE, asymmetric JWTs, JWKS rotation, and policy-based authorization (RBAC+ABAC). Validation denies unknown fields; mass assignment is prevented. I harden against XSS/CSRF/injection, disable prototype mutation, and add rate limits/backpressure. CI enforces SAST/DAST, dependency and secret scans; runtime emits audit events and alerts on anomalies. Deploys run non-root, read-only, with least privileges.”
Evaluation Criteria
- Validation rigor: Schema-first, covers body/query/params, denies unknown, normalizes inputs.
- Authentication depth: Short-lived asymmetric JWTs, refresh rotation, OIDC/PKCE, JWKS rotation.
- Authorization quality: RBAC/ABAC, tenant scoping, server-side filters, least privilege.
- Threat coverage: XSS/CSRF/injection/prototype pollution mitigations and safe headers/CORS.
- Operational security: Rate limiting, body limits, timeouts, safe logging, alerts.
- Supply chain/Runtime: Pinned deps, scans, non-root, read-only, minimal image, secrets management.
- Testing/CI: Unit/integration/abuse cases, OpenAPI contract checks, SAST/DAST gates.
Red flags: Long-lived HS256 tokens, open CORS with creds, trusting client IDs, localStorage tokens, no rotation or audit.
Preparation Tips
- Generate an OpenAPI spec and wire Ajv/Zod for request/response validation.
- Implement OIDC login → access (10m) + rotating refresh; verify tokens with JWKS.
- Build a policy module (RBAC+ABAC) that maps claims → permissions → SQL filters.
- Add CSP, strict CORS, HttpOnly cookies, and CSRF tokens where cookies are used.
- Write injection tests (SQL/NoSQL/regex), and block __proto__/constructor keys in deep merges.
- Configure rate limits, body limits, timeouts; simulate slowloris/large payloads.
- Set up CI with SAST/DAST, dependency and secret scanning; pin lockfiles.
- Run Node in a non-root container, read-only FS, minimal base; rotate keys.
- Create dashboards for 401/403 ratios, refresh reuse, and rate-limit hits; alert on spikes.
Real-world Context
Marketplace API: Lacked tenant scoping—users could query others’ orders. Fix: ABAC guard injecting tenant_id from claims into all queries plus policy tests; support tickets dropped sharply.
Fintech backend: Long-lived HS256 JWTs shared across services; key leak caused exposure. Migrated to RS256 with JWKS rotation, 10m TTL, refresh rotation with reuse detection.
SaaS uploads: Prototype pollution via __proto__ in JSON merge led to handler crash. Denylisted keys, used safe deep-merge, and enabled --disable-proto=throw.
Retail app: Open CORS with credentials caused token theft via malicious origin. Locked origins, added SameSite cookies and CSRF tokens; added CSP for admin HTML.
Ops win: Rate limits + body caps + timeouts absorbed scraper spikes without outages.
Key Takeaways
- Schema-first validation and deny-by-default prevent bad states early.
- Prefer OIDC + short-lived asymmetric JWTs with rotation; derive authZ from claims.
- Harden against XSS/CSRF/injection/prototype pollution; set strict headers/CORS.
- Add limits, backpressure, and observability to survive abuse.
- Lock supply chain and runtime: scans, least privilege, signed/rotated keys.
Practice Exercise
Scenario:
You’re shipping a multi-tenant Node.js API for projects and files. Requirements: schema validation, OIDC auth, RBAC+ownership checks, secure uploads, CSRF-safe web console, and resilience under scraping.
Tasks:
- Contracts & validation: Author OpenAPI for POST /projects, GET /files/:id, PATCH /me. Implement Ajv/Zod validators for params/query/body; deny unknown props; normalize strings.
- AuthN: Integrate OIDC (Auth Code + PKCE). Verify JWT with JWKS (RS256). Access TTL 10m; implement refresh rotation with reuse detection.
- AuthZ: Create policy middleware: role checks + ABAC (tenant_id from token). Auto-inject tenant filter into ORM queries; reject cross-tenant IDs.
- Uploads: Enforce MIME/size; randomize names; store outside webroot; virus-scan stub; signed download URLs.
- Threats: Add CSP, strict CORS per tenant domain; HttpOnly; SameSite=Lax for console cookies + CSRF tokens. Deny __proto__/constructor keys during deep merges; set body size limits, timeouts.
- Ops: Add rate limiting (IP + user), request IDs, structured logs (no tokens). Expose metrics: p95, 401/403 ratio, rate-limit hits.
- CI/CD: Secret scan, SAST/DAST, dep audit, OpenAPI contract tests; non-root container, read-only FS.
- Tests: Abuse cases (SQL/regex/proto keys), refresh reuse replay, CSRF attempt, cross-tenant access, oversize upload.
Deliverable:
A repo demonstrating secure Node.js API patterns with validators, OIDC+RBAC/ABAC, hardened uploads, CSRF-safe console, limits/observability, and automated security checks passing green.

