Security, performance and deployment
Validate input at the boundary, limit and time every call, profile before optimising, and ship a small container that shuts down cleanly.
Validation and injection
import { z } from "zod";
const CreateOrder = z.object({
sku: z.string().regex(/^[A-Z0-9-]{3,32}$/),
quantity: z.number().int().min(1).max(100),
});
app.post("/orders", async (req, res) => {
const parsed = CreateOrder.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: "invalid input" });
return res.status(201).json(await createOrder(parsed.data));
});
// the parameterised form: the driver sends the value, never the SQL text
await pool.query("SELECT * FROM orders WHERE sku = $1", [sku]);- Validate on the server even when the client already does; anything from the network is untrusted by definition.
- SQL, shell commands and file paths must never be assembled from input — parameterise, use argument arrays, and resolve paths against a fixed root.
- Reject rather than sanitise when the expected format is known: a SKU that does not match the pattern is a bug, not something to clean up.
- Encode output for its destination. A string that is safe in JSON can still be an XSS or a header injection in another context.
- Path traversal is the filesystem version of the same mistake: check that the resolved path still starts with the intended directory.
Limits and dependencies
// fixed-window limiter backed by Redis
async function allow(key, limit = 100, windowSeconds = 60) {
const bucket = "rl:" + key + ":" + Math.floor(Date.now() / 1000 / windowSeconds);
const count = await redis.incr(bucket);
if (count === 1) await redis.expire(bucket, windowSeconds);
return count <= limit;
}
res.setHeader("ratelimit-limit", "100");
res.setHeader("ratelimit-remaining", String(Math.max(0, limit - used)));| Control | Protects against | Detail |
|---|---|---|
| Input validation | Injection, corrupt data | At the boundary, before any side effect |
| Rate limiting | Brute force, scraping, cost spikes | Per account and key, not just per address |
| Timeouts everywhere | Hung sockets and threads | Outbound calls, queries, and the whole request |
| Body size limit | Memory exhaustion | Applies to JSON, forms and file uploads |
| Security headers | Clickjacking, MIME sniffing | CSP is the one that carries real weight |
| Lockfile and audit | Known vulnerable dependency versions | Patch cadence matters more than the report |
⚠️
Behind a proxy, the client address in the forwarded header is supplied by the caller. Key your rate limits on an authenticated identity or on the address your proxy sets, and never trust a header the internet can write.
Profiling and deployment
# build stage: dependencies and compilation never reach the runtime image
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# runtime stage: only what the process needs
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]node --cpu-prof --cpu-prof-dir=./prof src/server.js
node --heap-prof src/server.js
node --max-old-space-size=512 src/server.js # cap the heap below the container limit
node --report-on-signal --report-signal=SIGUSR2 src/server.js- Build one artifact and promote it through environments; rebuilding per environment means you never test what you ship.
- Run as a non-root user and add a
.dockerignoreso hostnode_modulesnever leak into the image. - Answer a health check and handle
SIGTERM— zero-downtime restarts depend on the platform knowing when you are ready and when you are done. - Profile before optimising. Most Node performance complaints turn out to be a missing index, an N+1 query, or unbounded concurrency rather than the runtime.
- Cap concurrency on every fan-out: a single request that fires a thousand queries will take the database down before it takes itself down.
FAQ
Is npm audit worth running?
Yes as a signal, no as a gate on its own. It sees known CVEs and misses everything else, and most findings sit in build tooling that never ships. Fix what is reachable at run time first, and keep dependencies few and current.
How do I find a memory leak in production?
Capture heap profiles a few minutes apart and compare the retained set, or send a signal-triggered report before the process is killed. A monotonically growing heap with flat traffic almost always means an unbounded cache, a listener added per request, or a closure holding a large buffer.
Related
Databases, HTTP clients and external services Worker threads, child processes and clustering
Last refreshed 2026-09-18.