Imagem de comparação entre NestJS e Express

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

Logo do 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

Logo do 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?

Express em ação

Vantagens do Express:

  1. Projetos pequenos ou MVPs: Se você precisa de algo rápido e funcional, Express permite iniciar com pouquíssimo código.

  2. Performance: Por ser mais leve, Express geralmente tem melhor performance bruta em aplicações simples.

  3. Flexibilidade: Liberdade para estruturar seu projeto como quiser e adicionar apenas o que precisa.

  4. Controle total: Você decide como organizar rotas, middlewares e lógica de negócio.

  5. 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?

NestJS em ação

Vantagens do NestJS:

  1. Projetos empresariais ou de larga escala: A arquitetura modular e estruturada facilita a manutenção à medida que o projeto cresce.

  2. Equipes maiores: A arquitetura opinativa garante consistência entre diferentes desenvolvedores.

  3. TypeScript nativo: Aproveite ao máximo o TypeScript com decoradores, interfaces e tipos.

  4. Injeção de dependências: Sistema robusto de injeção facilita testes e modularização.

  5. 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

Gráfico 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:

  1. Migração gradual: Crie uma aplicação NestJS e monte o Express dentro dela:

  2. 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.