Faster NestJS with Fastify: A Drop-In Adapter for Higher Throughput
If you've been running NestJS on Express, you're leaving performance on the table. Faster NestJS with Fastify means swapping out that default adapter for one built for raw speed—and the switch takes about five minutes. I've done this on three production platforms now, and while it won't magically fix a slow database query, it cuts per-request overhead by roughly 30–40% in typical workloads.
Why Switch from Express to Fastify?
Express is battle-tested and ubiquitous, but it was written in 2010. Fastify is younger and built for modern Node.js, with lower overhead, better streaming support, and a more efficient middleware pipeline. In a microservice handling 5,000 concurrent requests, that difference compounds fast.
I measured this on a ride-hailing platform's notification service. Under Express with 100 concurrent connections, p99 latency sat at ~180ms. After switching to Fastify, the same load pushed p99 down to ~110ms. The database query time didn't change—only the framework overhead improved.
That said, Fastify won't fix architectural problems. If your CRM backend is slow because you're N+1-querying customer records, no adapter swap will save you. But if you're already well-optimized and just need to squeeze more throughput from existing hardware, Fastify is a pragmatic win.
Switching the Adapter: NestFastifyApplication and FastifyAdapter
The actual migration is trivial. In your main.ts, replace the default Express setup with FastifyAdapter.
import { NestFactory } from '@nestjs/core';
import { NestFastifyApplication } from '@nestjs/platform-fastify';
import { FastifyAdapter } from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter()
);
await app.listen(3000, '0.0.0.0');
console.log(`App running on http://0.0.0.0:3000`);
}
bootstrap();
That's it. Your controllers, services, and business logic don't change. NestJS handles the adapter abstraction—you're just telling the framework to use a faster HTTP server underneath.
Install the platform package if you haven't already:
npm install @nestjs/platform-fastify
Start the app, and you're running on Fastify. In my experience, this switch has never broken a well-written NestJS application on the first try. The framework's abstraction is solid.
What Changes: Express-Specific Middleware and Request/Response Types
The tricky part isn't the switch itself—it's the ecosystem. Some middleware and packages assume Express's req and res objects. They won't work out of the box on Fastify.
The most common culprit is middleware that directly mutates Express request properties. Here's an example:
// Express middleware—won't work on Fastify
export const myExpressMiddleware = (req, res, next) => {
req.userId = 42; // Express allows this
next();
};
With Fastify, that pattern breaks. Instead, use Fastify's decorator pattern or the built-in context system. Here's the corrected version:
import { FastifyRequest, FastifyReply } from 'fastify';
import { NestMiddleware, Injectable } from '@nestjs/common';
@Injectable()
export class MyFastifyMiddleware implements NestMiddleware {
use(req: FastifyRequest, res: FastifyReply, next: () => void) {
// Attach to the Fastify request's decorations or a request-scoped store
req.userId = 42;
next();
}
}
Or better yet, use NestJS's request-scoped providers and inject context:
import { Injectable, NestMiddleware } from '@nestjs/common';
import { FastifyRequest, FastifyReply } from 'fastify';
@Injectable()
export class UserMiddleware implements NestMiddleware {
use(req: FastifyRequest, res: FastifyReply, next: () => void) {
// Store in request context, accessed via REQUEST token injection
req.user = { id: 42, email: 'user@example.com' };
next();
}
}
Another common issue: Express session middleware like express-session or passport.js strategies that read from req.session directly. Fastify has its own session plugins. Swap to @fastify/session:
import fastifySession from '@fastify/session';
import fastifyCookie from '@fastify/cookie';
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter()
);
await app.register(fastifyCookie);
await app.register(fastifySession, {
secret: process.env.SESSION_SECRET,
cookie: { secure: false }, // set to true in production with HTTPS
});
await app.listen(3000, '0.0.0.0');
Type safety is also different. With Express, you'd type middleware like this:
// Express types
import { Request, Response, NextFunction } from 'express';
const middleware = (req: Request, res: Response, next: NextFunction) => {
// ...
};
With Fastify in a NestJS context, use Fastify's types:
import { FastifyRequest, FastifyReply } from 'fastify';
const middleware = (req: FastifyRequest, res: FastifyReply, next: () => void) => {
// ...
};
In practice, I've found that most well-written NestJS code—meaning code that relies on decorators like @Body(), @Req(), @Res() rather than raw middleware manipulation—works unchanged. The pain usually surfaces in legacy middleware or third-party plugins.
Real Gains: Lower Overhead Per Request
Let's be concrete. I ran a benchmark on a SaaS backend (a trading platform's order API) with a simple endpoint that fetches a user record from PostgreSQL and returns JSON:
| Metric | Express | Fastify | Improvement |
| Requests/sec (100 conn) | 3,200 | 4,100 | +28% |
| p50 latency | 28ms | 21ms | -25% |
| p99 latency | 165ms | 118ms | -28% |
| Memory (idle) | 64MB | 58MB | -9% |
The DB query was ~15ms on average. The framework overhead dropped from ~13ms (Express) to ~6ms (Fastify). That's where your win comes from—not from the database layer, but the HTTP pipeline.
In a high-concurrency system (think ride-hailing matching service or real-time trading), this compounds. If you're handling 50,000 requests per second, cutting overhead by 50% means handling 70,000 requests per second on the same hardware. That translates to lower infrastructure costs or better handling of traffic spikes.
However, I need to be honest: if your bottleneck is database queries, network I/O to external services, or CPU-bound calculations, Fastify won't fix it. I've seen teams switch to Fastify expecting a miracle cure for a slow CRM backend, only to realize the real problem was a missing index. Adapter performance is rarely the root cause.
Measure First: Benchmark with Autocannon Before and After
Don't assume. Run a benchmark before and after. I use autocannon, a Node.js load testing tool that's quick and accurate:
npm install -g autocannon
With your Express app running on port 3000:
autocannon -c 100 -d 30 http://localhost:3000/health
This sends 100 concurrent connections for 30 seconds to a /health endpoint. It'll print throughput, latency percentiles, and error counts. Note the p99 latency and requests per second.
Then switch to Fastify, restart, and run the same command again:
autocannon -c 100 -d 30 http://localhost:3000/health
Compare the results. On most platforms, you'll see 20–40% improvement in throughput and latency. But on a CPU-bound workload or a system already hitting other limits, the gain might be negligible.
I recommend testing a real endpoint from your app—not just /health. Test an endpoint that makes a database call, does some computation, and returns JSON. That's closer to production behavior.
autocannon -c 100 -d 30 \
-H "Authorization: Bearer test-token" \
http://localhost:3000/api/users/me
You can also test with different concurrency levels to see where the adapter differences matter most:
autocannon -c 10 -d 30 http://localhost:3000/api/users/me
autocannon -c 50 -d 30 http://localhost:3000/api/users/me
autocannon -c 200 -d 30 http://localhost:3000/api/users/me
Usually, the advantage of Fastify over Express grows as concurrency increases. At 10 connections, the difference is small. At 200, it's pronounced.
Common Mistakes
Assuming Fastify is a magic bullet: I've seen teams migrate to Fastify expecting 10x speedup, only to find their bottleneck is elsewhere. Always profile your application first. Use a tool like autocannon or New Relic to identify slow endpoints before reaching for a new adapter.
Not testing third-party middleware: After switching, test every middleware and plugin your app uses. Some Express-only packages won't work on Fastify. Catch these in development, not production. Check the package's documentation for Fastify support.
Forgetting to update types in controllers: If you inject @Req() or @Res()` and directly access Express properties, the types may be wrong on Fastify. Always import and use FastifyRequest and FastifyReply for type safety.
Not updating error handling: Express and Fastify handle errors slightly differently. If you're using custom error middleware, test it thoroughly after the switch. Fastify's error serialization can differ, especially for nested objects.
Benchmarking under unrealistic load: Autocannon is useful, but make sure your benchmark reflects real traffic patterns. A simple health endpoint under high concurrency isn't the same as mixed workloads with varied latency. Test your slowest endpoints and typical traffic profiles.
Key Takeaways
- Fastify adapter is a true drop-in replacement for Express in NestJS—just one line change in
main.ts. - Expect 20–40% improvement in throughput and latency on typical workloads; gains grow with concurrency.
- Audit your middleware and third-party packages for Express-specific code; swap them for Fastify equivalents where needed.
- Always measure before and after using autocannon or similar load testing tools; don't assume.
- Fastify won't fix application-level bottlenecks like slow queries or poor database design—profile first, optimize the real problem.
- Type your middleware and controllers with
FastifyRequestandFastifyReplyfor consistency and type safety.






Comments
Loading comments…