Safeguard
Application Security

Choosing a secure Node.js web framework: Express, Fastify, Koa, and NestJS compared

None of Express, Fastify, Koa, or NestJS enable security headers, CSRF protection, or input validation by default — the defaults you inherit differ more than you'd think.

Safeguard Research Team
Research
6 min read

Express, Fastify, Koa, and NestJS together power a large share of production Node.js APIs, and all four ship with the same blind spot: none of them set a security header, verify a CSRF token, or validate a request body until you add something on top. Express's own security best-practices page (expressjs.com/en/advanced/best-practice-security) calls adding helmet "the single highest-impact change" a team can make, which is a striking admission for a framework that sits among the most downloaded packages on npm. The gap is not evenly distributed, though. Fastify bakes JSON Schema validation into its routing layer via Ajv v8, so a misshaped request body can be rejected structurally rather than caught deep in business logic. NestJS wraps Express or Fastify and adds a ValidationPipe built on class-validator, but that pipe is opt-in, not global, until a developer wires it in main.ts. And csurf, the CSRF middleware nearly every Express tutorial written before 2022 tells you to install, is now a deprecated, unmaintained package with a known unpatched vulnerability — meaning a security decision made by copying a five-year-old blog post can quietly ship a broken defense today. This piece compares what each framework actually gives you before you write a line of security code, and what you still have to add yourself.

What does Express give you by default, and what's missing?

Express gives you almost nothing security-specific out of the box — no security headers, no CSRF protection, and no input validation. The framework's own documentation is explicit about this, recommending helmet as the standard fix for headers: it sets roughly 13 HTTP response headers, including Content-Security-Policy, Cross-Origin-Opener-Policy, and Cross-Origin-Resource-Policy, none of which Express sets on its own. CSRF protection is worse off. csurf, the package most Express guides still reference, was deprecated by its maintainers after they identified a vulnerability they were not able to fix and stopped maintaining the module, with the README now pointing users toward alternatives like csrf-csrf. Input validation is entirely opt-in via third-party libraries such as express-validator, joi, or zod — Express itself has no concept of a request schema. A team that installs Express and starts routing requests is, by design, starting from zero on all three fronts.

Does Fastify's built-in schema validation actually change the risk picture?

Yes — Fastify's JSON Schema validation, powered by Ajv v8, is a structural advantage the other three frameworks lack, because a route can declare a schema object and get request validation "for free" as part of routing rather than as a separate middleware step a developer has to remember to add. That said, Fastify does not extend this built-in posture to headers or CSRF. Security headers require the first-party @fastify/helmet plugin, and CSRF protection requires @fastify/csrf-protection — and that plugin's own documentation explicitly recommends pairing it with @fastify/helmet, since csrf-protection only handles token generation and verification, not the response headers that harden a browser session against related attacks. So Fastify closes one gap (validation) structurally while leaving the other two (headers, CSRF) exactly as manual as Express.

Why does Koa require the most manual security work?

Koa requires the most manual work because it was deliberately built as a minimal core with no router, no body parser, no security headers, and no CSRF handling bundled in — everything is middleware you choose and compose yourself. Where Express at least documents a canonical helmet recommendation, Koa's ecosystem equivalent, koa-helmet, and its CSRF equivalent, koa-csrf, are separate decisions a team has to make and keep current independently, with no single official security guide steering that choice the way Express's does. This isn't a flaw in Koa's design philosophy — minimalism is the point, and it gives teams full control over their middleware stack — but it does mean Koa has the least opinionated defaults of the four, and a team's actual security posture depends entirely on which packages someone remembered to add during initial setup rather than on anything the framework enforces.

What does NestJS add on top of its underlying HTTP adapter?

NestJS runs on top of Express by default (or Fastify, if configured), so it inherits whichever adapter's baseline it's built on — NestJS itself adds no additional network-facing security by default. Its official documentation shows adding helmet as global middleware in main.ts, the same pattern an Express app would use, and its CSRF guidance has historically pointed toward the same now-deprecated csurf package that plagued plain Express projects, meaning teams following older NestJS tutorials may have inherited that same unpatched dependency. NestJS's genuine structural improvement is on input validation: its ValidationPipe, paired with class-validator decorators on DTO classes, can validate every incoming request body against a typed class definition. The key caveat is that this pipe is not registered by default — a developer has to call app.useGlobalPipes(new ValidationPipe()) explicitly, and a NestJS app with that line omitted has the exact same unvalidated-input problem as a bare Express route.

What's the practical takeaway for choosing between them?

The practical takeaway is that framework choice changes how much manual setup a team owes itself, not whether that setup is required — every one of these four frameworks needs helmet-equivalent headers, an actively maintained CSRF library, and an explicit validation layer added before it should handle untrusted traffic. Fastify's schema validation and NestJS's DTO-based ValidationPipe give teams a stronger structural starting point for input handling than Express or Koa, but none of the four close the CSRF or header gap without an explicit dependency choice, and any team whose setup script still installs csurf is running unmaintained protection today, regardless of framework. The safest process is to treat security defaults as a checklist to verify on every new service scaffold — headers, CSRF, and validation — rather than an assumption inherited from whichever framework's tutorial a team happened to follow.

How Safeguard helps

Missing security headers and unvalidated input reaching a dangerous sink are exactly the classes of misconfiguration Safeguard's application security testing is built to catch, not just in theory but on the running service. Safeguard's DAST engine sends safe, non-destructive requests against a verified target and flags missing security headers directly — catching an Express or Koa deployment that never added helmet, or a @fastify/csrf-protection setup that was wired without its companion headers plugin. Safeguard's SAST engine complements that by tracing untrusted input from a source, like a request parameter, to a dangerous sink across your JavaScript/TypeScript codebase, surfacing the exact route handler where validation was skipped, whether that's a raw Express route or a NestJS controller with ValidationPipe left unregistered. Because SAST and DAST findings share one correlation model, a DAST-confirmed missing header or unvalidated endpoint gets linked back to the source-code location that produced it, turning a framework's silent default into a findable, fixable ticket instead of a gap nobody notices until it's exploited.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.