Fastify and NestJS are both modern answers to "what do I use instead of raw Express?" but they answer the question in opposite ways. Fastify is a direct Express replacement, same shape, ~2-3× the throughput, with built-in JSON schema validation. NestJS is a full application framework with modules, DI, and enterprise patterns baked in.
The decision usually comes down to whether you want a fast HTTP server (Fastify) or an opinionated architecture (NestJS). This guide walks through both on their merits and introduces a third option that handles what neither covers, infrastructure and cross-service type safety.
| Aspect | Fastify | NestJS |
|---|---|---|
| First release | 2016 | 2017 |
| Philosophy | Fast HTTP + schema-first | Opinionated architecture + DI |
| Throughput | ~45k req/sec | ~28k req/sec (Express adapter), ~45k (Fastify adapter) |
| Validation | JSON Schema (built-in) | class-validator (built-in) |
| TypeScript | Great (typebox generics) | First-class (required in practice) |
| Architecture | Plugins + hooks | Modules, controllers, providers |
| Learning curve | Low | Medium-high |
| Ecosystem | Plugin-based, maintained core plugins | @nestjs/* official packages + third-party |
| Typical use | High-throughput APIs, microservices | Enterprise backends, monoliths |
Fastify is a plugin-based HTTP server. You register plugins (which can themselves register routes, decorators, or sub-plugins) and the framework gives you a fast router with hooks for every request lifecycle stage.
import Fastify from "fastify";
const app = Fastify({ logger: true });
app.get<{ Params: { id: string } }>("/users/:id", async (req, reply) => {
const user = await db.users.findById(req.params.id);
if (!user) return reply.status(404).send({ error: "not found" });
return user;
});
app.listen({ port: 3000 });
The code shape is very close to Express. The difference is under the hood, Fastify's router and JSON serializer are much faster, and schema validation is a first-class concern.
NestJS gives you a DI container and a convention for everything. Every endpoint lives in a controller, every bit of business logic in a provider, every related set of providers in a module.
// users.controller.ts
import { Controller, Get, Param, NotFoundException } from "@nestjs/common";
import { UsersService } from "./users.service";
@Controller("users")
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(":id")
async findOne(@Param("id") id: string) {
const user = await this.usersService.findOne(id);
if (!user) throw new NotFoundException();
return user;
}
}
Plus a service class and a module declaration. More code per endpoint, but a consistent structure across the whole app.
NestJS can run on Fastify instead of Express. Switching is a one-line change:
// main.ts
import { NestFactory } from "@nestjs/core";
import { FastifyAdapter } from "@nestjs/platform-fastify";
const app = await NestFactory.create(AppModule, new FastifyAdapter());
This gets you NestJS's architecture with Fastify's performance, at the cost of some plugin compatibility (not every Express middleware works, and NestJS-specific tooling assumes Express by default).
Fastify's selling point. On identical hardware running a simple JSON endpoint:
Fastify is 50% faster than Express for bare HTTP, mostly from a faster router, a faster JSON stringifier (using pre-compiled schemas), and fewer middleware layers. In real workloads where you're waiting on a database, this difference often disappears into I/O wait. On CPU-bound API workloads or at very high RPS, it's meaningful.
NestJS on the Fastify adapter gets Fastify's throughput with NestJS's ergonomics. If you want both, that's the combo.
Fastify validates requests against JSON Schema, which also lets it compile a fast serializer for responses.
app.post<{ Body: { email: string; name: string } }>(
"/users",
{
schema: {
body: {
type: "object",
required: ["email", "name"],
properties: {
email: { type: "string", format: "email" },
name: { type: "string", minLength: 1 },
},
},
},
},
async (req, reply) => {
// req.body is validated and typed
},
);
With Typebox, you can author schemas in TypeScript and get static types for free:
import { Type } from "@sinclair/typebox";
const CreateUser = Type.Object({
email: Type.String({ format: "email" }),
name: Type.String({ minLength: 1 }),
});
app.post("/users", { schema: { body: CreateUser } }, async (req) => {
// req.body typed automatically
});
NestJS uses decorators on DTO classes:
import { IsEmail, IsString, MinLength } from "class-validator";
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(1)
name: string;
}
@Post()
create(@Body() dto: CreateUserDto) {
// dto validated before this runs
}
Both approaches validate at the framework boundary. The Fastify approach has one nice property: the schema is the source of truth for both validation and response serialization, and can be published as OpenAPI. The NestJS approach is more natural if you already think in classes and DI.
Fastify has a plugin model. Core plugins (@fastify/cors, @fastify/helmet, @fastify/cookie, @fastify/swagger) are maintained by the Fastify team and are well-aligned with the framework's hooks model. The plugin ecosystem is smaller than Express's but more coherent.
NestJS has @nestjs/* official packages for most things you'd want (config, GraphQL, WebSockets, microservices, TypeORM, Mongoose, Passport, throttling, caching, Bull). Third-party NestJS modules are common, and Express/Fastify middleware can be wrapped into NestJS's plugin system.
If you want a specific integration, Passport auth, TypeORM, GraphQL, queues, NestJS usually has an official module for it. Fastify has plugins that work but you'll do more wiring.
Fastify gives you .inject() for in-process request testing without a live server, which is fast and clean:
const response = await app.inject({
method: "GET",
url: "/users/1",
});
expect(response.statusCode).toBe(200);
NestJS's @nestjs/testing lets you instantiate a testing module with mocked providers via the DI container:
const module = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: { findOne: jest.fn().mockResolvedValue({ id: "1" }) },
},
],
}).compile();
Both are good. NestJS is nicer for unit tests (mocking dependencies is a one-liner). Fastify is nicer for integration tests (inject is fast and requires no setup).
Fastify keeps the mental model small. You know what app.get(...) does. Adding a new feature usually means writing one route handler. The downside: you pick your own ORM, auth, testing conventions, folder structure. In a team, this becomes inconsistent.
NestJS has more ceremony per endpoint but removes a lot of decisions. Every team member writes code the same way. You get tooling (nest generate, auto-wiring, schematics) that speeds up common tasks once you're past the learning curve.
Time-to-first-endpoint favors Fastify. Time-to-consistent-20-endpoint-API favors NestJS.
Use Fastify when:
Use NestJS when:
Both Fastify and NestJS stop at the HTTP layer. Everything else (your database, Pub/Sub, cron jobs, deployment, cross-service type safety, distributed tracing), is yours to figure out. For a small single-service API that's fine. For anything larger, it means assembling a framework from npm packages.
Encore takes a different approach: you declare services and infrastructure as typed objects in TypeScript, and Encore handles the rest. The database, the queue, the API contract, the local dev environment, the production deployment on AWS or GCP, all generated from your code.
The endpoint that took a decorated controller in NestJS or a schema-validated handler in Fastify is one function in Encore:
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
// Provisions managed Postgres, Docker locally, RDS or Cloud SQL in production.
const db = new SQLDatabase("users", { migrations: "./migrations" });
interface User {
id: number;
email: string;
name: string;
}
export const get = api(
{ method: "GET", path: "/users/:id", expose: true },
async ({ id }: { id: number }): Promise<User> => {
return await db.queryRow`SELECT * FROM users WHERE id = ${id}`;
},
);
Types are the contract. Encore validates incoming requests against them, generates an OpenAPI spec, produces a typed client SDK, and traces every request through the system automatically.
What Encore handles that neither Fastify nor NestJS does:
ClientProxy.send returning anyencore run spins up services, databases, and Pub/Sub locally with one commandEncore is open source with 11,000+ GitHub stars and runs in production at companies including Groupon.
Want to jump straight to a running app? Clone this starter and deploy it to your own cloud.
Encore uses a Rust-based request router and runs the business logic in Node. On the same hardware, Encore hits numbers comparable to raw Fastify for simple endpoints, and beats NestJS (Express adapter) significantly. For most applications, framework throughput is a non-issue, your DB and network calls dominate.
For a new TypeScript backend in 2026:
For an existing codebase, migrate only if you're hitting real pain the current stack doesn't solve.
# Fastify
npm init -y && npm install fastify
# NestJS
npm install -g @nestjs/cli && nest new project-name
# Encore
brew install encoredev/tap/encore
encore app create my-app --example=ts/empty
cd my-app && encore run
Want to jump straight to a running app? Clone this starter and deploy it to your own cloud.