Fastify for Performance
NestJS is platform-agnostic at the HTTP layer: by default it runs on top of Express, but it can run on Fastify with almost no changes to your application code. Fastify is built around a low-overhead routing engine and schema-based serialization, which makes it one of the fastest Node.js web frameworks available. For request-heavy APIs, swapping the underlying adapter is often the single highest-leverage performance change you can make.
Why Fastify is faster
Two design decisions drive Fastify’s throughput advantage. First, its router uses a radix tree (find-my-way) that resolves routes in near-constant time regardless of how many routes you register. Second, and more importantly, Fastify compiles JSON Schemas into purpose-built serialization functions with fast-json-stringify. Instead of calling the generic, reflective JSON.stringify on every response, Fastify runs a function that already knows the exact shape of your payload — frequently 2-3x faster on large objects.
Express, by contrast, leans on middleware chains and JSON.stringify, which are flexible but carry more per-request overhead.
Installing the adapter
Install the platform package alongside Fastify itself:
npm install @nestjs/platform-fastify
You can then remove @nestjs/platform-express if you no longer need it, though keeping it installed is harmless.
Switching the bootstrap
The only mandatory change is in your entry file. Pass a FastifyAdapter instance to NestFactory.create and type the application as NestFastifyApplication.
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({ logger: false }),
);
// Fastify binds to 127.0.0.1 by default; use '0.0.0.0' for containers.
await app.listen(3000, '0.0.0.0');
}
bootstrap();
Note the second argument to
listen. Express defaults to all interfaces, but Fastify binds tolocalhostonly. Inside Docker or Kubernetes you must pass'0.0.0.0'or the service will be unreachable.
Your controllers, providers, modules, pipes, and guards remain unchanged. NestJS abstracts the request/response objects, so idiomatic code keeps working.
import { Controller, Get, Param } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
}
What must change versus Express
Most differences only surface if you reached for the raw platform objects or Express-specific middleware.
| Concern | Express | Fastify |
|---|---|---|
| Native request/response | Express.Request / Response | FastifyRequest / FastifyReply |
| Middleware ecosystem | helmet, cors, compression | Use @fastify/* plugins where possible |
| Static assets | ServeStaticModule (express) | @fastify/static via the adapter |
| File uploads | multer | @fastify/multipart |
| Sending a response manually | res.status(200).send() | reply.status(200).send() |
If you inject the native response with @Res(), update the type:
import { Controller, Get, Res } from '@nestjs/common';
import { FastifyReply } from 'fastify';
@Controller('health')
export class HealthController {
@Get()
check(@Res() reply: FastifyReply) {
reply.status(200).send({ status: 'ok' });
}
}
Register Fastify plugins through the adapter’s underlying instance:
import helmet from '@fastify/helmet';
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.register(helmet);
Schema-based serialization
To unlock Fastify’s biggest win, attach a JSON Schema to your routes so it can compile a fast serializer. You can declare the schema directly on a route via the adapter, or — more idiomatically in Nest — model responses and let Fastify infer fast paths for primitives. A focused, hand-written schema looks like this:
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
const instance = app.getHttpAdapter().getInstance();
instance.addSchema({
$id: 'user',
type: 'object',
properties: {
id: { type: 'string' },
email: { type: 'string' },
createdAt: { type: 'string', format: 'date-time' },
},
});
Routes that reference { $ref: 'user#' } in their response schema serialize through compiled code instead of JSON.stringify.
Benchmarking the difference
Never trust a performance change you have not measured. Use a load generator such as autocannon against the same endpoint on both adapters.
npx autocannon -c 100 -d 20 http://localhost:3000/users/42
Output:
Running 20s test @ http://localhost:3000/users/42
100 connections
Express adapter
Req/Sec 18,420
Latency 5.31 ms
Fastify adapter
Req/Sec 31,870
Latency 3.02 ms
Numbers vary by hardware, payload size, and middleware, but a 40-70% throughput gain on JSON-heavy endpoints is typical. Run each test several times, discard the warm-up run, and keep the machine otherwise idle so the comparison is fair.
Best practices
- Always pass
'0.0.0.0'tolistenwhen deploying in containers — the Fastify default oflocalhostis a common silent outage. - Prefer
@fastify/*plugins over Express middleware; mixing ecosystems forfeits much of the speed advantage. - Attach JSON Schemas to high-traffic routes so Fastify can compile dedicated serializers.
- Disable the Fastify logger (
{ logger: false }) in production and route logging through your own structured logger to avoid double work. - Benchmark before and after with
autocannon, comparing identical endpoints under identical load. - Audit
@Res()usage and native request typing during the migration — these are the only places application code typically breaks.