DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on • Originally published at stacknotice.com

NestJS vs Fastify (2026): When Full Framework Beats Micro-Framework

NestJS and Fastify are not competing for the same niche. Fastify is an HTTP framework — routing, request lifecycle, JSON serialization. NestJS is an application framework — all of that plus dependency injection, module encapsulation, decorators, and a strong opinion about how every file should be organized.

The comparison isn't really about performance numbers. It's about whether you want the framework to enforce architecture, or whether your team can enforce it themselves.

Architecture: Yours vs. Theirs

Fastify: You Own the Structure

// src/plugins/db.ts
export const dbPlugin = fp(async (fastify) => {
  const pool = new Pool({ connectionString: process.env.DATABASE_URL })
  fastify.decorate('db', drizzle(pool, { schema }))
  fastify.addHook('onClose', async () => pool.end())
})

// src/routes/users.ts
export const userRoutes: FastifyPluginAsync = async (fastify) => {
  fastify.post('/', {
    schema: {
      body: Type.Object({
        name: Type.String({ minLength: 1 }),
        email: Type.String({ format: 'email' })
      }),
      response: { 201: UserResponse }
    }
  }, async (request, reply) => {
    const existing = await fastify.db.query.users.findFirst({
      where: (u, { eq }) => eq(u.email, request.body.email)
    })
    if (existing) return reply.status(409).send({ error: 'Email already in use' })
    const [user] = await fastify.db.insert(users).values(request.body).returning()
    return reply.status(201).send(user)
  })
}
Enter fullscreen mode Exit fullscreen mode

Clean for senior developers. The risk: on larger teams, architectural drift accumulates over months.

NestJS: Module → Controller → Service

// src/users/users.service.ts
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly userRepo: Repository<User>
  ) {}

  async create(dto: CreateUserDto): Promise<User> {
    const existing = await this.userRepo.findOne({ where: { email: dto.email } })
    if (existing) throw new ConflictException('Email already in use')
    return this.userRepo.save(this.userRepo.create(dto))
  }
}

// src/users/users.controller.ts
@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post() @HttpCode(201) @UseGuards(JwtAuthGuard)
  create(@Body() dto: CreateUserDto) {
    return this.usersService.create(dto)
  }
}
Enter fullscreen mode Exit fullscreen mode

A developer joining day one knows exactly where the business logic for any endpoint lives. That predictability has real value on a 10-person team.

Dependency Injection: The Real Differentiator

NestJS's DI container changes how you test:

// NestJS unit test — inject mocks without touching production code
describe('UsersService', () => {
  const mockRepo = {
    findOne: jest.fn(),
    create: jest.fn((dto) => dto),
    save: jest.fn((user) => ({ id: 'uuid-123', ...user }))
  }

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: getRepositoryToken(User), useValue: mockRepo }
      ]
    }).compile()
    service = module.get(UsersService)
  })

  it('throws ConflictException when email is taken', async () => {
    mockRepo.findOne.mockResolvedValueOnce({ id: 'existing' })
    await expect(service.create({ name: 'Test', email: 'taken@test.com' }))
      .rejects.toThrow(ConflictException)
  })
})
Enter fullscreen mode Exit fullscreen mode

Mocking is natural. The DI container handles wiring — tests override what they need without changing anything else.

NestJS + Fastify Adapter

NestJS can run Fastify underneath instead of Express — you get both:

// main.ts
const app = await NestFactory.create<NestFastifyApplication>(
  AppModule,
  new FastifyAdapter({ logger: true })
)
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }))
await app.listen(3000, '0.0.0.0')
Enter fullscreen mode Exit fullscreen mode

Throughput improvement: ~30-50% more req/s on JSON-heavy routes. Verify Express-specific middleware compatibility before switching an existing app.

Performance

Setup ~Throughput
Fastify standalone ~75,000 req/s
NestJS + Fastify adapter ~50,000 req/s
NestJS + Express (default) ~28,000 req/s

Context: NestJS's DI resolves at startup, not per request. For most production APIs, database latency (5-50ms) dominates over framework overhead (0.1-0.5ms). The gap matters at 10k+ sustained req/s.

Auto-Generated Swagger

NestJS's killer feature for enterprise APIs:

export class CreateUserDto {
  @ApiProperty({ description: 'Full name', minLength: 1 })
  @IsString() @MinLength(1)
  name: string

  @ApiProperty({ format: 'email' })
  @IsEmail()
  email: string
}
// swagger-ui at /api — no separate spec file to maintain
Enter fullscreen mode Exit fullscreen mode

Decision Framework

Choose Fastify if:

  • Small team (1-5 devs) with strong architectural discipline
  • Maximum throughput, minimal abstraction overhead
  • Microservice with a clear, bounded scope
  • Migrating from Express and want a familiar model with real performance gains

Choose NestJS if:

  • Team of 5+ with mixed experience levels
  • You need enforced architecture to prevent drift at scale
  • Enterprise features: microservices, GraphQL, WebSockets, gRPC out of the box
  • Coming from Spring Boot or Angular — mental model maps directly
  • Auto-generated Swagger without maintaining a spec

Choose NestJS + Fastify adapter if:

  • You want NestJS's structure but need better throughput than Express delivers

Full article at stacknotice.com/blog/nestjs-vs-fastify-2026

Top comments (0)