Introdução
Como desenvolvedor fullstack TypeScript, uma das decisões mais importantes que enfrentamos ao iniciar um novo projeto backend é: qual framework escolher? No universo Node.js, Express e NestJS são dois dos frameworks mais populares, cada um com sua própria filosofia e características.
Express tem sido o padrão da indústria por muitos anos - leve, minimalista e extremamente flexível. Por outro lado, o NestJS chegou para trazer estrutura, escalabilidade e recursos avançados inspirados no Angular. Ambos têm seus méritos, mas qual escolher e quando?
Neste post, vou compartilhar minha experiência prática trabalhando com os dois frameworks, analisando seus pontos fortes, fracos, e casos de uso ideais.
Uma visão geral dos frameworks
Express
Express é um framework web minimalista para Node.js, lançado em 2010 e que se tornou a base para muitos outros frameworks. É conhecido por:
- Filosofia minimalista e não opinativa
- Foco em performance e simplicidade
- Curva de aprendizado suave
- Extrema flexibilidade na estruturação de projetos
NestJS
NestJS é um framework mais recente, construído por cima do Express (por padrão), que traz conceitos de arquitetura do Angular para o backend:
- Estrutura opinativa com arquitetura clara
- Suporte nativo para TypeScript
- Sistema de módulos, decoradores e injeção de dependências
- Ferramentas integradas para testes, validação, documentação, etc.
Comparação de código: Criando uma API simples
Vamos comparar como seria implementar uma API REST simples em ambos os frameworks.
Express
// app.js
const express = require('express');
const app = express();
const port = 3000;
app.use(express.json());
// Simulando um banco de dados
const users = [
{ id: 1, name: 'Johan' },
{ id: 2, name: 'Maria' }
];
// Rotas
app.get('/users', (req, res) => {
res.json(users);
});
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ message: 'Usuário não encontrado' });
res.json(user);
});
app.post('/users', (req, res) => {
const newUser = {
id: users.length + 1,
name: req.body.name
};
users.push(newUser);
res.status(201).json(newUser);
});
app.listen(port, () => {
console.log(`Servidor rodando em http://localhost:${port}`);
});
NestJS
// users.controller.ts
import { Controller, Get, Post, Param, Body, NotFoundException } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
const user = this.usersService.findOne(+id);
if (!user) {
throw new NotFoundException('Usuário não encontrado');
}
return user;
}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
}
// users.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable()
export class UsersService {
private users = [
{ id: 1, name: 'Johan' },
{ id: 2, name: 'Maria' }
];
findAll() {
return this.users;
}
findOne(id: number) {
return this.users.find(user => user.id === id);
}
create(createUserDto: CreateUserDto) {
const newUser = {
id: this.users.length + 1,
...createUserDto
};
this.users.push(newUser);
return newUser;
}
}
// create-user.dto.ts
export class CreateUserDto {
name: string;
}
// app.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users/users.controller';
import { UsersService } from './users/users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class AppModule {}
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
Observando os exemplos acima, a diferença fica evidente: Express é mais simples e direto, enquanto NestJS introduz mais estrutura e separação de responsabilidades.
Quando escolher Express?
Vantagens do Express:
Projetos pequenos ou MVPs: Se você precisa de algo rápido e funcional, Express permite iniciar com pouquíssimo código.
Performance: Por ser mais leve, Express geralmente tem melhor performance bruta em aplicações simples.
Flexibilidade: Liberdade para estruturar seu projeto como quiser e adicionar apenas o que precisa.
Controle total: Você decide como organizar rotas, middlewares e lógica de negócio.
Microserviços simples: Para microserviços com funções bem definidas e limitadas.
Código Express na prática: Middleware personalizado
// Middleware de logging personalizado
app.use((req, res, next) => {
console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
next();
});
// Middleware de autenticação
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization;
if (!token || token !== 'meu-token-secreto') {
return res.status(401).json({ message: 'Não autorizado' });
}
next();
};
// Aplicando middleware apenas em rotas específicas
app.get('/users', authMiddleware, (req, res) => {
res.json(users);
});
Quando escolher NestJS?
Vantagens do NestJS:
Projetos empresariais ou de larga escala: A arquitetura modular e estruturada facilita a manutenção à medida que o projeto cresce.
Equipes maiores: A arquitetura opinativa garante consistência entre diferentes desenvolvedores.
TypeScript nativo: Aproveite ao máximo o TypeScript com decoradores, interfaces e tipos.
Injeção de dependências: Sistema robusto de injeção facilita testes e modularização.
Recursos integrados: Documentação automática com Swagger, validação, logging estruturado, etc.
Código NestJS na prática: Guards e Interceptors
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization;
if (!token || token !== 'meu-token-secreto') {
throw new UnauthorizedException();
}
return true;
}
}
// logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const method = request.method;
const url = request.url;
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`${method} ${url} - ${Date.now() - now}ms`)),
);
}
}
// Aplicando Guard e Interceptor
@UseGuards(AuthGuard)
@UseInterceptors(LoggingInterceptor)
@Controller('users')
export class UsersController {
// ...
}
Comparação de performance
Em testes que realizei, Express tende a ter melhor performance bruta em aplicações simples devido à sua natureza minimalista:
- Express: Mais rápido em throughput bruto de requisições por segundo
- NestJS: Ligeiramente mais lento devido à camada adicional de abstração
Porém, a diferença geralmente é pequena e a escalabilidade de código do NestJS muitas vezes compensa essa pequena perda de performance.
Qual escolher para seu próximo projeto?
Para facilitar sua decisão, criei uma tabela comparativa:
Característica | Express | NestJS |
---|---|---|
Curva de aprendizado | Baixa | Moderada a alta |
Velocidade de desenvolvimento inicial | Muito rápida | Requer mais configuração inicial |
Escalabilidade de código | Depende da estrutura adotada | Excelente |
Performance | Excelente | Muito boa |
Tipagem | Necessita configuração adicional | TypeScript nativo |
Documentação | Concisa | Extensa e detalhada |
Testes | Configuração manual | Ferramentas integradas |
Comunidade | Muito grande e madura | Grande e crescendo rapidamente |
Minha experiência prática
Na SparkWebStudios , utilizamos Express para:
- Microserviços simples
- APIs que precisam de performance máxima
- Protótipos e POCs
- Lambdas e funções serverless
E NestJS para:
- Sistemas complexos com múltiplos módulos
- Projetos que precisarão escalar em código e equipe
- APIs que precisam de documentação robusta
- Quando queremos aproveitar ao máximo o TypeScript
Dicas práticas para migração
Se você já tem um projeto Express e quer migrar para NestJS:
Migração gradual: Crie uma aplicação NestJS e monte o Express dentro dela:
Migração por módulos: Migre uma funcionalidade de cada vez para módulos NestJS.
// main.ts em NestJS
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import * as express from 'express';
import { AppModule } from './app.module';
async function bootstrap() {
const expressApp = express();
// Configure seu app Express aqui
expressApp.get('/legacy', (req, res) => {
res.send('Esta é uma rota legada');
});
const app = await NestFactory.create(
AppModule,
new ExpressAdapter(expressApp),
);
await app.listen(3000);
}
bootstrap();
Conclusão
Depois de trabalhar com ambos os frameworks em diversos projetos, cheguei à conclusão de que:
- Express continua sendo excelente para projetos pequenos, MVPs ou quando performance bruta é crítica
- NestJS se destaca em projetos de médio a grande porte, especialmente quando trabalha com equipes ou precisa de uma arquitetura robusta
A melhor notícia? Você não precisa escolher exclusivamente um ou outro. Como NestJS usa Express por baixo dos panos (por padrão), você pode inclusive misturar os dois quando necessário!
Qual framework você prefere? Tem alguma experiência para compartilhar? Deixe nos comentários abaixo!
Se você gostou deste conteúdo, me siga para mais posts sobre desenvolvimento fullstack, TypeScript e boas práticas.
Você também pode conferir meu portfólio ou me encontrar no LinkedIn.