import ComparisonTable from ’../../components/ComparisonTable.astro’;
Node.js frameworks range from minimalist (Fastify, Express) to full-featured (NestJS). NestJS and Fastify represent two strong philosophies: structured enterprise architecture vs. high-performance minimal core.
Quick Verdict
Choose NestJS if: You’re building a team-maintained API, want Angular-style structure with decorators, or need a comprehensive framework with built-in solutions for auth, validation, queues, and more.
Choose Fastify if: You need maximum throughput, are building microservices with minimal footprint, or want a fast core without opinionated structure.
Feature Comparison
<ComparisonTable headers={[“Feature”, “NestJS”, “Fastify”]} rows={[ [“Architecture”, “Opinionated (modules, DI)”, “Minimal, bring your own”], [“TypeScript”, “First-class (built in)”, “First-class (built in)”], [“Performance”, “~50K req/s”, “~80K req/s”], [“HTTP adapter”, “Express or Fastify”, “Native”], [“Dependency injection”, “Built-in (Angular-style)”, “Via plugin (fastify-awilix)”], [“Validation”, “class-validator built-in”, “fastify-type-provider-typebox”], [“OpenAPI/Swagger”, “@nestjs/swagger”, “fastify-swagger”], [“Testing”, “Jest, testing utilities built-in”, “Jest, tap”], [“Microservices”, “Built-in transport layer”, “Manual”], [“GraphQL”, “@nestjs/graphql”, “mercurius plugin”], [“WebSockets”, “@nestjs/websockets”, “fastify-websocket”], ]} />
Application Structure
NestJS — Angular-inspired module system:
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // Other modules can inject UsersService
})
export class UsersModule {}
// src/app.module.ts
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRoot({ ... }),
UsersModule,
AuthModule,
ProductsModule,
],
})
export class AppModule {}
Fastify — file-based or manual organization:
// src/routes/users/index.ts
import { FastifyPluginAsync } from 'fastify';
import { UserService } from '../../services/UserService';
const usersRoutes: FastifyPluginAsync = async (fastify) => {
const userService = new UserService(fastify.db);
fastify.get('/', async (request, reply) => {
const users = await userService.findAll();
return users;
});
fastify.post('/', async (request, reply) => {
const user = await userService.create(request.body);
reply.status(201).send(user);
});
};
export default usersRoutes;
// src/app.ts
fastify.register(usersRoutes, { prefix: '/users' });
fastify.register(productsRoutes, { prefix: '/products' });
NestJS enforces structure. Fastify gives you flexibility to organize however you want.
Dependency Injection
NestJS DI — Angular-style decorators:
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async signIn(email: string, password: string): Promise<{ access_token: string }> {
const user = await this.usersService.findByEmail(email);
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
throw new UnauthorizedException('Invalid credentials');
}
const payload = { sub: user.id, email: user.email, role: user.role };
return { access_token: await this.jwtService.signAsync(payload) };
}
}
// AuthController uses AuthService — NestJS injects automatically
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
signIn(@Body() { email, password }: LoginDto) {
return this.authService.signIn(email, password);
}
}
Fastify with awilix DI (optional):
import Fastify from 'fastify';
import { fastifyAwilixPlugin, diContainer } from '@fastify/awilix';
import { asClass, asValue } from 'awilix';
import { UserService } from './services/UserService';
const fastify = Fastify();
await fastify.register(fastifyAwilixPlugin, {
disposeOnClose: true,
disposeOnResponse: true,
});
diContainer.register({
userService: asClass(UserService).singleton(),
db: asValue(db),
});
fastify.post('/auth/login', async (request) => {
const { userService } = request.diScope.cradle;
return userService.signIn(request.body);
});
Fastify’s DI requires a plugin. NestJS DI is built-in and enforced by the framework.
Request Validation
NestJS — class-validator + class-transformer:
// dto/create-user.dto.ts
import { IsEmail, IsString, MinLength, IsEnum, IsOptional } from 'class-validator';
import { Transform } from 'class-transformer';
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
}
export class CreateUserDto {
@IsString()
@MinLength(2)
name: string;
@IsEmail()
@Transform(({ value }) => value.toLowerCase())
email: string;
@IsString()
@MinLength(8)
password: string;
@IsEnum(UserRole)
@IsOptional()
role?: UserRole = UserRole.USER;
}
// Controller — validation applied via pipe
@Post()
@UsePipes(new ValidationPipe({ whitelist: true }))
async create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
Fastify — TypeBox or JSON Schema:
import { Type, Static } from '@sinclair/typebox';
const CreateUserSchema = Type.Object({
name: Type.String({ minLength: 2 }),
email: Type.String({ format: 'email' }),
password: Type.String({ minLength: 8 }),
role: Type.Optional(Type.Union([
Type.Literal('admin'),
Type.Literal('user'),
])),
});
type CreateUserBody = Static<typeof CreateUserSchema>;
fastify.post<{ Body: CreateUserBody }>(
'/',
{
schema: {
body: CreateUserSchema,
},
},
async (request, reply) => {
const user = await userService.create(request.body);
reply.status(201).send(user);
}
);
Fastify validates at JSON Schema level (very fast). NestJS uses class-validator (more expressive, supports custom decorators).
Authentication Example
NestJS JWT auth (complete guard system):
// auth/jwt.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// auth/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) return true;
const { user } = context.switchToHttp().getRequest();
return roles.includes(user.role);
}
}
// Decorators for authorization
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// In controller
@Get('admin-only')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
adminEndpoint() {
return { data: 'admin only' };
}
Fastify hooks:
import { FastifyRequest, FastifyReply } from 'fastify';
import { verifyJWT } from './auth';
// Global auth hook
fastify.addHook('preHandler', async (request, reply) => {
const publicRoutes = ['/auth/login', '/auth/register'];
if (publicRoutes.includes(request.url)) return;
try {
const token = request.headers.authorization?.replace('Bearer ', '');
request.user = await verifyJWT(token);
} catch {
reply.status(401).send({ error: 'Unauthorized' });
}
});
// Route-level auth
fastify.get('/admin', {
preHandler: [requireRole('admin')],
}, async (request) => {
return { data: 'admin only' };
});
Performance Benchmark
| Scenario | NestJS (Fastify adapter) | Fastify (direct) |
|---|---|---|
| Hello world | ~70K req/s | ~80K req/s |
| JSON serialization | ~55K req/s | ~75K req/s |
| DB query + response | ~18K req/s | ~20K req/s |
Performance difference narrows significantly when actual database work is involved. NestJS can use Fastify as its HTTP adapter, which closes most of the gap.
// NestJS with Fastify adapter
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
Microservices
NestJS built-in transport layer:
// NestJS microservice with Redis transport
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.REDIS,
options: {
host: 'localhost',
port: 6379,
},
},
);
// Message handler
@MessagePattern('user.created')
async handleUserCreated(@Payload() data: CreateUserEvent) {
await this.notificationService.sendWelcomeEmail(data.email);
}
// Emit events from another service
this.client.emit('user.created', { email: user.email, name: user.name });
NestJS’s microservice support (Redis, NATS, Kafka, gRPC, MQTT transports) is extensive and requires minimal configuration.
When to Choose Each
Choose NestJS:
- Team projects with multiple developers
- Enterprise APIs needing clear structure
- Applications that grow complex (auth, queues, webhooks, caching)
- Teams familiar with Angular’s patterns
- Need built-in microservice communication
Choose Fastify:
- Maximum throughput matters (API gateway, high-volume services)
- Small team or solo project where structure is flexible
- Microservices with minimal responsibility
- When you want fine-grained control over every piece
- Building something simple that doesn’t need DI/modules
Bottom Line
NestJS is the better choice for most team-built backends — the structure it enforces pays dividends as complexity grows, the ecosystem is comprehensive, and TypeScript integration is seamless. Fastify is genuinely faster and excellent for performance-critical services where you’re willing to provide your own structure. Many architecture use both: NestJS for the main API, Fastify for performance-sensitive microservices.