

Engineering
NestJS vs Express vs Fastify for SaaS (2026)
You've picked Node.js for your SaaS backend, probably for the reason we set out in Django vs Node.js: one language and one type system from the database to the browser. Then the framework question arrives, along with a decade of conflicting tutorials and benchmark charts. Before you choose between NestJS, Express and Fastify, ask whether your MVP needs a separate backend at all.
Which Node.js framework should a SaaS backend use?
For most SaaS MVPs built on Next.js, the right Node.js framework is none yet: route handlers and server actions are your backend. When you genuinely need a standalone API service, pick Fastify for a small team, NestJS when several engineers will share the codebase for years, and Express mainly when you're extending code that already runs on it.
Hono is the fourth serious option, best when you deploy to the edge or want a structured API inside a Next.js app. What doesn't decide it is raw speed: all four handle far more traffic than an early-stage SaaS will send them, and one database query costs more than the gap between them. Decide on structure, type safety and who will maintain the code.
Do you need a separate backend at all?
Usually not, while your only client is your own Next.js web app. Route handlers give you ordinary HTTP endpoints for Stripe webhooks and anything an outside caller needs; server actions handle mutations from your own React components. Same repository, same deploy, shared types, one hosting bill. That's the backend layer of our SaaS MVP tech stack, and plenty of products never outgrow it.
A dedicated API service starts paying for itself when one of these is true:
- A mobile app ships. Native clients need a stable, versioned API, because you can't force users to update. Server actions are designed to be called from your own React code, not from an iOS app.
- You're publishing a public API. Integration partners need documented, rate-limited, versioned endpoints: a product in its own right.
- Several frontends share the same logic, and one service beats three copies drifting apart.
- Connections are long-lived. WebSockets and streaming at volume sit awkwardly on serverless functions and comfortably on a long-running Node process.
Background jobs are the trigger founders cite most, and the weakest on its own: a queue-backed worker or a hosted service like Inngest or Trigger.dev handles slow work without an HTTP framework in sight. There's also a middle path: Hono can be mounted inside one Next.js catch-all route handler, giving you middleware and validation without a second deployment.
How do NestJS, Express, Fastify and Hono compare?
As of October 2026 the current releases on npm are Express 5.3, Fastify 5.12, NestJS 12.1 and Hono 4.13.
| Express 5 | Fastify 5 | NestJS 12 | Hono 4 | |
|---|---|---|---|---|
| What it is | Minimal HTTP framework, the long-time default | Schema-driven framework built for low overhead | Full application framework on top of Express or Fastify | Small Web Standards framework for many runtimes |
| Structure | None: you design it | Light: encapsulated plugins | Heavy: modules, controllers, providers | Light: routers and middleware |
| TypeScript | Community types via @types/express | Bundled types, schema-driven type providers | TypeScript-first, decorator-based | Written in TypeScript, typed request context |
| Performance | Last of seven in Fastify's published Node.js benchmark | First in its own benchmark | Its adapter's speed plus framework overhead | Close behind Fastify on Node.js in the same benchmark |
| Validation | Bring your own, usually Zod | Built in: JSON Schema via Ajv, official Zod and TypeBox type providers | class-validator pipes, or Standard Schema (Zod, Valibot) since v12 | Validator middleware for Zod or any Standard Schema library |
| Dependency injection | None | None: plugins and decorators instead | Built-in DI container | None |
| Testing | No built-in helper; Supertest is the usual add-on | Built-in inject(), no network needed | Testing module that swaps providers | app.request(), no server needed |
| Ecosystem | Largest middleware ecosystem in Node.js | Large official @fastify plugin set | First-party modules for GraphQL, config, microservices | Built-in middleware; smaller third-party pool |
| Learning curve | Lowest | Low to moderate | Steepest | Low |
| Hiring pool | Deepest: nearly every Node.js developer knows it | Smaller, but Express developers adapt fast | Good; familiar to Angular, Spring and .NET developers | Smallest, but quick to learn |
| Multi-tenant fit | Workable, but every guard is your discipline | Strong: hooks scoped by encapsulation | Strong structure; avoid request-scoped providers | Strong: typed per-request variables |
Express is no longer the stagnant project its reputation suggests. Express 5
shipped in September 2024 and has been the default npm install since March
2025, with 4.x still supported. Its headline fix: a rejected promise in an
async handler now reaches your error middleware without a manual .catch(next)
on every route. Everything else (layout, validation, errors, logging) is your
call, which is freedom on day one and archaeology by month eighteen.
Fastify is schema-first. Each route declares schemas for its body, params,
query and response. By default Fastify validates input against JSON Schema with
Ajv and serialises output with fast-json-stringify; the official Zod and
TypeBox type providers let that same schema type your handler too. Response
schemas also guard against leaks: fields you didn't declare aren't serialised,
so a stray passwordHash never reaches the client. Pino logging is built in,
and plugins give you modular boundaries without a DI container.
NestJS is a framework on top of a framework. You write modules, controllers
and injectable providers, and Nest runs them on Express by default or on
Fastify through its FastifyAdapter. Version 12, released in August 2026, moved
the core packages to ESM and added Standard Schema validation, so a Zod schema
can plug straight into @Body(), ending the old duplication between Zod types
and class-validator DTOs. The cost is ceremony. One CRUD endpoint touches a
module, a controller, a service and a DTO, which is overhead for two engineers
racing to launch and coherence for eight working in parallel.
Hono is built on the standard Request and Response objects and has zero
dependencies, so the same code runs on Cloudflare Workers, Deno, Bun, Vercel,
AWS Lambda and Node.js. Its third-party ecosystem is thinner for Node-specific
needs, but its typed context and validators make for a compact, pleasant API.
Is Fastify really faster than Express?
Yes, but by less than its reputation suggests, and not by enough to decide a SaaS build. Fastify's benchmark page, updated on 12 October 2026, puts Fastify 5.12 at about 89,000 requests per second against about 62,000 for Express 5.2, roughly 1.4 times faster, with Hono at about 85,000. The page itself labels the figures illustrative. NestJS's documentation is more generous, saying Fastify achieves "nearly twice the benchmark results" of Express.
Those are hello-world numbers on a single instance. A real SaaS request verifies a token, opens a transaction and runs a few queries, and the framework's share of that time is small. At MVP stage the bottleneck is your database and how well it's indexed. Choose Fastify for its schemas and plugins, and treat the speed as a bonus.
How do you handle multi-tenancy in a Node.js backend?
Resolve the tenant once from the authenticated session, keep it in request-scoped context, and let Postgres Row-Level Security enforce it. The framework only changes where that code lives. Never trust a tenant ID the client sends in a header or body; take it from the organisation claim in a verified token. Your data layer then sets the tenant inside each transaction, so the RLS policies from our multi-tenant SaaS architecture guide do the enforcing. In Fastify, the resolution step looks like this:
import Fastify from 'fastify'
import { verifySession } from './auth' // your auth provider's token check
declare module 'fastify' {
interface FastifyRequest { tenantId: string }
}
const app = Fastify({ logger: true })
app.decorateRequest('tenantId', '')
app.addHook('onRequest', async (req, reply) => {
const session = await verifySession(req.headers.authorization)
if (!session?.orgId) return reply.code(401).send({ error: 'No organisation' })
req.tenantId = session.orgId
})
Each framework has its own home for the same idea:
- Express: a middleware puts the tenant on
res.localsor a typedreqproperty. Only router order guarantees it runs before every route. - Fastify: the hook above. Register it inside the plugin that wraps your
authenticated routes, and encapsulation keeps it off Stripe webhooks and
health checks.
@fastify/request-contextadds AsyncLocalStorage-backed storage for code deep in the stack. - NestJS: a guard or middleware resolves the tenant. The tempting next
step, a request-scoped provider, is the trap: request scope "bubbles up the
injection chain", rebuilding controllers and services on every request.
Nest's documentation
warns this affects performance, and calls its durable-provider workaround
"not ideal for applications with a large number of tenants". Use
AsyncLocalStorage instead, typically via
nestjs-cls. - Hono:
createMiddlewarewith a typedVariablesmap; set the tenant withc.set, read it fromc.var, and use the built-incontextStorage()middleware to reach it outside handlers.
Whichever you choose, one place resolves the tenant, one place sets the database setting, and an automated test proves tenant A can't read tenant B's rows.
Does the framework change your API style or architecture?
Less than you'd think: all four serve REST comfortably, and none of them requires microservices. For GraphQL, NestJS has a first-party module and Fastify pairs with the Mercurius adapter, but most MVPs with one web client are better served by REST or tRPC, as our REST vs GraphQL comparison explains.
NestJS also ships a microservices package, which makes splitting your backend feel like the done thing. Resist it: one service with clear domain modules is easier to run, test and change, as we argue in monolith vs microservices. Nest modules and Fastify plugins both leave seams to split along later.
Our default: Next.js first, Fastify when we split
For the SaaS products we build, the backend starts inside Next.js: route handlers for webhooks and anything an outside caller needs, server actions for the app's own mutations. It runs on the stack we use everywhere: Next.js, Postgres (usually on Supabase), Clerk, Stripe and Resend, deployed on Vercel. We add no second service until one of the triggers above is real.
When one is, we reach for Fastify with TypeScript and the official Zod type provider, for three reasons:
- Schemas do triple duty. One Zod schema validates the request, types the
handler and, through
@fastify/swagger, generates the OpenAPI document your mobile developers or integration partners build against. - Structure without ceremony. A route is a function plus a schema, and encapsulated plugins give each domain a boundary without dependency injection. Any developer who has used Express is productive almost at once.
- It fits the reason you split. Fastify runs as a long-lived Node process on a container host such as Railway or Render, which is what WebSockets and long-running requests need.
The service verifies the same Clerk session tokens in an onRequest hook,
uses the same Postgres database and RLS policies, and shares its Zod schemas
with the Next.js app through a workspace package.
We deviate in three cases. NestJS when the client's in-house team will own the codebase with several engineers, or already knows Nest or Angular; we run it on the Fastify adapter with singleton providers. Express when we inherit a working Express codebase: we upgrade it to version 5 rather than rewrite it. Hono when the API targets Cloudflare Workers or the edge, or should live inside a Next.js app.
The trade-off we accept is that Fastify won't enforce an architecture the way Nest does. Folder conventions and plugin boundaries are ours to keep.
Frequently asked questions
Is Express still a good choice in 2026?
Yes for maintaining existing apps and for small services, but it's rarely our pick for a new SaaS API. Express 5 fixed async error handling and version 4 is still supported, so nothing is forcing a migration, and almost every Node.js developer already knows it. What it lacks is built-in validation, types and structure: on a new build you'd add all three yourself, while Fastify and Hono provide them out of the box.
Is NestJS overkill for an MVP?
For a team of one to three engineers, usually yes. Modules, providers, decorators and dependency injection add files and concepts to every feature, slowing the first weeks of a build exactly when speed matters most. Nest earns its keep when several engineers work in parallel and need shared conventions, or when an in-house team will maintain the codebase for years.
Can NestJS run on Fastify instead of Express?
Yes: Nest uses Express by default, and switching means installing
@nestjs/platform-fastify and passing a FastifyAdapter when you create the
app. Do it at the start of a project: code that reaches into Express-specific
request and response objects needs rewriting later. One gotcha from Nest's
docs: Fastify listens only on localhost by default, so pass '0.0.0.0' to
listen() when running in a container, or nothing outside it can connect.
Should I use Hono instead of Fastify?
Use Hono when you deploy to Cloudflare Workers, Bun or Deno, or want an API mounted inside Next.js; use Fastify for a long-running Node.js service. Hono's Web Standards design lets the same code move between runtimes, and on Node.js it benchmarks close to Fastify. Fastify has the deeper Node-specific plugin set and a longer production record, with version 1.0 released in 2018.
Want a backend sized to what your product actually needs?
The right answer might be no separate backend at all, or a small Fastify service beside your Next.js app, depending on your clients and your team. Book a free scoping call and we'll map the backend to your real requirements, then quote the build fixed-price, so you know the cost before you commit.




Leave a comment