Backend

Express.js

Referência completa de Express.js: roteamento, middleware, segurança, upload, TypeScript e estrutura de projeto

Instalação e Setup

# Criar projeto do zero
mkdir meu-api && cd meu-api
npm init -y

# Instalar Express
npm install express

# Instalar tipos para TypeScript (opcional)
npm install -D typescript @types/node @types/express ts-node nodemon

# Dependências comuns de um projeto real
npm install express cors helmet morgan dotenv express-rate-limit multer
npm install express-validator compression cookie-parser
// index.js — Hello World mínimo
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.json({ message: 'Hello World' });
});

app.listen(3000, () => {
  console.log('Servidor rodando na porta 3000');
});

Setup com variáveis de ambiente

// Carregar .env antes de qualquer coisa
require('dotenv').config();

const express = require('express');
const app = express();

const PORT = process.env.PORT || 3000;
const NODE_ENV = process.env.NODE_ENV || 'development';

app.listen(PORT, () => {
  console.log(`[${NODE_ENV}] Servidor na porta ${PORT}`);
});
# .env
PORT=3000
NODE_ENV=development
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
JWT_SECRET=supersegredo

Middleware Fundamentais (parsers)

const express = require('express');
const app = express();

// Parsear JSON no body (application/json)
app.use(express.json());

// Parsear form data (application/x-www-form-urlencoded)
app.use(express.urlencoded({ extended: true }));

// Servir arquivos estáticos
app.use(express.static('public'));

// Parsear cookies (requer: npm install cookie-parser)
const cookieParser = require('cookie-parser');
app.use(cookieParser());

Roteamento Básico

Métodos HTTP

const app = require('express')();

// GET — buscar recurso
app.get('/users', (req, res) => {
  res.json([{ id: 1, name: 'Alice' }]);
});

// POST — criar recurso
app.post('/users', (req, res) => {
  const { name, email } = req.body;
  // lógica de criação...
  res.status(201).json({ id: 2, name, email });
});

// PUT — substituir recurso completo
app.put('/users/:id', (req, res) => {
  const { id } = req.params;
  const data = req.body;
  res.json({ id, ...data });
});

// PATCH — atualizar parcialmente
app.patch('/users/:id', (req, res) => {
  const updates = req.body;
  res.json({ updated: updates });
});

// DELETE — remover recurso
app.delete('/users/:id', (req, res) => {
  res.status(204).send(); // No Content
});

// HEAD — igual GET, mas sem body (útil para checar existência)
app.head('/users/:id', (req, res) => {
  res.status(200).end();
});

// OPTIONS — listar métodos permitidos
app.options('/users', (req, res) => {
  res.set('Allow', 'GET,POST,OPTIONS').status(200).end();
});

Parâmetros de Rota

// Parâmetro obrigatório: /users/42
app.get('/users/:id', (req, res) => {
  const { id } = req.params; // '42' (sempre string)
  const idNum = parseInt(id, 10);
  res.json({ id: idNum });
});

// Múltiplos parâmetros: /users/42/posts/7
app.get('/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params;
  res.json({ userId, postId });
});

// Parâmetro opcional com regex: /files/foto.jpg
app.get('/files/:filename(\\w+\\.\\w+)', (req, res) => {
  res.send(`Arquivo: ${req.params.filename}`);
});

// Wildcard: captura qualquer coisa após /api/
app.get('/api/*', (req, res) => {
  res.json({ path: req.params[0] }); // ex: 'users/list'
});

Query Strings

// GET /search?q=express&page=2&limit=10
app.get('/search', (req, res) => {
  const {
    q = '',           // valor padrão vazio
    page = 1,
    limit = 10,
    sort = 'created_at',
    order = 'desc'
  } = req.query;

  // query string sempre chega como string, converta quando necessário
  const pageNum = parseInt(page, 10);
  const limitNum = Math.min(parseInt(limit, 10), 100); // teto de 100

  res.json({
    query: q,
    pagination: { page: pageNum, limit: limitNum },
    sorting: { field: sort, order }
  });
});

// Arrays via query string: /filter?tags[]=node&tags[]=express
app.get('/filter', (req, res) => {
  // com express.urlencoded({extended:true}), arrays funcionam
  const { tags = [] } = req.query;
  res.json({ tags: Array.isArray(tags) ? tags : [tags] });
});

Express Router

O Router permite modularizar as rotas e separar responsabilidades por domínio.

Criando um Router

// routes/users.js
const { Router } = require('express');
const router = Router();

// Middleware específico desse router
router.use((req, res, next) => {
  console.log(`[Users Router] ${req.method} ${req.path}`);
  next();
});

router.get('/', async (req, res, next) => {
  try {
    const users = await UserService.findAll();
    res.json(users);
  } catch (err) {
    next(err); // passa pro error handler global
  }
});

router.get('/:id', async (req, res, next) => {
  try {
    const user = await UserService.findById(req.params.id);
    if (!user) return res.status(404).json({ error: 'Usuário não encontrado' });
    res.json(user);
  } catch (err) {
    next(err);
  }
});

router.post('/', async (req, res, next) => {
  try {
    const user = await UserService.create(req.body);
    res.status(201).json(user);
  } catch (err) {
    next(err);
  }
});

router.put('/:id', async (req, res, next) => {
  try {
    const user = await UserService.update(req.params.id, req.body);
    res.json(user);
  } catch (err) {
    next(err);
  }
});

router.delete('/:id', async (req, res, next) => {
  try {
    await UserService.delete(req.params.id);
    res.status(204).send();
  } catch (err) {
    next(err);
  }
});

module.exports = router;
// app.js — registrar routers
const express = require('express');
const app = express();

app.use(express.json());

// Prefixo /api/v1
app.use('/api/v1/users', require('./routes/users'));
app.use('/api/v1/posts', require('./routes/posts'));
app.use('/api/v1/auth', require('./routes/auth'));

Router com mergeParams

// routes/comments.js — precisa do :postId do router pai
const { Router } = require('express');
// mergeParams: true permite acessar params do router pai
const router = Router({ mergeParams: true });

router.get('/', async (req, res) => {
  const { postId } = req.params; // vem do pai!
  const comments = await CommentService.findByPost(postId);
  res.json(comments);
});

module.exports = router;
// app.js
const postsRouter = Router();
postsRouter.use('/:postId/comments', require('./routes/comments'));
app.use('/posts', postsRouter);
// agora: GET /posts/42/comments funciona

Middleware

Middleware são funções (req, res, next) que interceptam o ciclo de requisição.

Middleware Global

const morgan = require('morgan');
const compression = require('compression');

// Logger de requisições HTTP
app.use(morgan('dev')); // 'combined' para produção

// Comprimir respostas (gzip)
app.use(compression());

// Middleware customizado: adicionar timestamp
app.use((req, res, next) => {
  req.requestTime = new Date().toISOString();
  next(); // SEMPRE chame next() ou a requisição trava
});

// Middleware customizado: log detalhado
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url} — ${req.ip}`);
  next();
});

Middleware por Rota

// Middleware de autenticação
const authMiddleware = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: 'Token não fornecido' });
  }
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded; // injeta usuário no req
    next();
  } catch {
    res.status(401).json({ error: 'Token inválido' });
  }
};

// Aplicar apenas em rotas específicas
app.get('/profile', authMiddleware, (req, res) => {
  res.json(req.user);
});

// Aplicar em múltiplas rotas via array
app.use(['/admin', '/dashboard'], authMiddleware);

Middleware de Validação com express-validator

const { body, validationResult } = require('express-validator');

// Regras de validação (são middlewares)
const createUserRules = [
  body('name')
    .notEmpty().withMessage('Nome é obrigatório')
    .isLength({ min: 2, max: 100 }).withMessage('Nome deve ter 2-100 caracteres'),
  body('email')
    .isEmail().withMessage('Email inválido')
    .normalizeEmail(),
  body('password')
    .isLength({ min: 8 }).withMessage('Senha mínima: 8 caracteres')
    .matches(/[A-Z]/).withMessage('Senha deve ter ao menos uma maiúscula')
    .matches(/\d/).withMessage('Senha deve ter ao menos um número'),
  body('age')
    .optional()
    .isInt({ min: 0, max: 120 }).withMessage('Idade inválida'),
];

// Middleware que verifica os erros
const validate = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({
      error: 'Dados inválidos',
      details: errors.array()
    });
  }
  next();
};

// Uso: as regras + validate antes do handler
app.post('/users', createUserRules, validate, async (req, res) => {
  const user = await UserService.create(req.body);
  res.status(201).json(user);
});

A Ordem Importa (Order Matters)

// ATENÇÃO: middleware é executado na ordem de registro

app.use(morgan('dev'));           // 1. log da requisição
app.use(helmet());               // 2. headers de segurança
app.use(express.json());         // 3. parsear body
app.use(cors(corsOptions));      // 4. CORS

// Rotas registradas DEPOIS dos middlewares globais
app.use('/api', router);

// Error handler SEMPRE por último (4 parâmetros)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({ error: err.message });
});

Middleware de Erro

// Error handler global — DEVE ter exatamente 4 parâmetros
// Qualquer middleware que chama next(err) cai aqui
app.use((err, req, res, next) => {
  // Log detalhado
  console.error({
    message: err.message,
    stack: err.stack,
    url: req.url,
    method: req.method,
    body: req.body,
    timestamp: new Date().toISOString()
  });

  // Tratar erros conhecidos
  if (err.name === 'ValidationError') {
    return res.status(422).json({ error: err.message });
  }
  if (err.name === 'UnauthorizedError') {
    return res.status(401).json({ error: 'Não autorizado' });
  }
  if (err.code === 'LIMIT_FILE_SIZE') {
    return res.status(413).json({ error: 'Arquivo muito grande' });
  }

  // Erro genérico (não expor stack em produção)
  const status = err.status || err.statusCode || 500;
  res.status(status).json({
    error: process.env.NODE_ENV === 'production'
      ? 'Erro interno do servidor'
      : err.message
  });
});

Request e Response

Objeto req (Request)

app.post('/exemplo', (req, res) => {
  // Parâmetros de rota (/users/:id)
  console.log(req.params);       // { id: '42' }
  console.log(req.params.id);    // '42'

  // Query string (?page=2&limit=10)
  console.log(req.query);        // { page: '2', limit: '10' }

  // Body (requer express.json() ou urlencoded())
  console.log(req.body);         // { name: 'Alice', email: '...' }

  // Headers
  console.log(req.headers);      // objeto com todos os headers
  console.log(req.get('Content-Type'));  // 'application/json'
  console.log(req.headers.authorization); // 'Bearer xyz...'

  // Informações da requisição
  console.log(req.method);       // 'POST'
  console.log(req.path);         // '/exemplo'
  console.log(req.url);          // '/exemplo?foo=bar'
  console.log(req.originalUrl);  // URL original antes de roteamento
  console.log(req.hostname);     // 'localhost'
  console.log(req.ip);           // '::1' ou '127.0.0.1'
  console.log(req.ips);          // array quando atrás de proxy
  console.log(req.protocol);     // 'http' ou 'https'
  console.log(req.secure);       // true se HTTPS
  console.log(req.xhr);          // true se chamada AJAX

  // Cookies (requer cookie-parser)
  console.log(req.cookies);      // { sessionId: 'abc...' }
  console.log(req.signedCookies);

  // Verificar Content-Type
  if (req.is('application/json')) { /* ... */ }

  res.json({ ok: true });
});

Objeto res (Response)

app.get('/exemplos', (req, res) => {
  // ---- JSON ----
  res.json({ data: 'valor' });
  res.status(201).json({ created: true });
  res.status(400).json({ error: 'Requisição inválida' });

  // ---- Texto ----
  res.send('Hello World');
  res.send('<h1>HTML direto</h1>');

  // ---- Status ----
  res.status(204).send();        // No Content
  res.sendStatus(404);           // envia status + texto padrão "Not Found"

  // ---- Headers ----
  res.set('X-Custom-Header', 'valor');
  res.set({
    'Cache-Control': 'no-cache',
    'X-Request-Id': req.id
  });
  res.type('application/json');  // define Content-Type

  // ---- Redirect ----
  res.redirect('/nova-url');           // 302 por padrão
  res.redirect(301, '/url-permanente'); // 301 Moved Permanently
  res.redirect('back');                 // volta para o referrer

  // ---- Arquivo ----
  res.sendFile('/caminho/absoluto/arquivo.pdf');
  res.download('/arquivo.pdf', 'relatorio.pdf'); // força download
  res.attachment('arquivo.csv');  // define header para download

  // ---- Render (template engine) ----
  res.render('index', { title: 'Home', user: req.user });

  // ---- Cookie ----
  res.cookie('sessionId', 'abc123', {
    httpOnly: true,    // não acessível via JS
    secure: true,      // apenas HTTPS
    sameSite: 'strict',
    maxAge: 24 * 60 * 60 * 1000 // 1 dia em ms
  });
  res.clearCookie('sessionId');

  // ---- JSON com cabeçalhos extras ----
  res
    .status(200)
    .set('X-Total-Count', '42')
    .set('X-Page', '1')
    .json({ data: [] });
});

Arquivos Estáticos

const path = require('path');
const express = require('express');
const app = express();

// Servir pasta 'public' na raiz
app.use(express.static('public'));
// Acesso: http://localhost:3000/imagem.jpg (arquivo em public/imagem.jpg)

// Com prefixo de URL
app.use('/static', express.static('public'));
// Acesso: http://localhost:3000/static/imagem.jpg

// Caminho absoluto (recomendado)
app.use('/assets', express.static(path.join(__dirname, 'assets')));

// Com opções
app.use('/static', express.static('public', {
  maxAge: '1d',           // cache de 1 dia no browser
  etag: true,             // suporte a ETag
  lastModified: true,     // header Last-Modified
  index: 'index.html',    // arquivo padrão em diretório
  dotfiles: 'ignore',     // ignorar .env, .htaccess etc
  redirect: false,        // não redirecionar ao acessar diretório sem /
  fallthrough: true       // passa pro próximo middleware se não encontrar
}));

// SPA: servir index.html para qualquer rota não encontrada
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

Template Engines

// Instalar: npm install pug ejs handlebars express-handlebars

const express = require('express');
const app = express();

// ---- EJS ----
app.set('view engine', 'ejs');
app.set('views', './views'); // pasta das views

app.get('/home', (req, res) => {
  res.render('home', {
    title: 'Meu Site',
    user: { name: 'Alice' },
    items: ['a', 'b', 'c']
  });
});
<!-- views/home.ejs -->
<!DOCTYPE html>
<html>
<head><title><%= title %></title></head>
<body>
  <h1>Olá, <%= user.name %></h1>
  <ul>
    <% items.forEach(item => { %>
      <li><%= item %></li>
    <% }) %>
  </ul>
</body>
</html>
// ---- Pug ----
app.set('view engine', 'pug');

// views/home.pug:
// html
//   head
//     title= title
//   body
//     h1 Olá #{user.name}

// ---- Handlebars ----
const { engine } = require('express-handlebars');
app.engine('hbs', engine({ extname: '.hbs' }));
app.set('view engine', 'hbs');
app.set('views', './views');

Tratamento de Erros

Erros Síncronos

// Express captura erros síncronos lançados no handler automaticamente
app.get('/sync-error', (req, res) => {
  throw new Error('Erro síncrono'); // capturado pelo Express
});

Erros Assíncronos

// Para async/await, você DEVE usar try/catch + next(err)
app.get('/async-error', async (req, res, next) => {
  try {
    const data = await someAsyncOperation();
    res.json(data);
  } catch (err) {
    next(err); // passa para o error handler
  }
});

// Wrapper para evitar repetição de try/catch
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Uso limpo:
app.get('/users', asyncHandler(async (req, res) => {
  const users = await UserService.findAll(); // se lançar, cai no error handler
  res.json(users);
}));

// Biblioteca pronta: npm install express-async-errors
require('express-async-errors'); // monkey-patches automaticamente
app.get('/users', async (req, res) => {
  const users = await UserService.findAll(); // erros vão para next() automaticamente
  res.json(users);
});

Erro Customizado

// Classe base para erros da aplicação
class AppError extends Error {
  constructor(message, statusCode = 500, code = null) {
    super(message);
    this.name = 'AppError';
    this.statusCode = statusCode;
    this.code = code;
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  constructor(resource = 'Recurso') {
    super(`${resource} não encontrado`, 404, 'NOT_FOUND');
  }
}

class ValidationError extends AppError {
  constructor(message, details = []) {
    super(message, 422, 'VALIDATION_ERROR');
    this.details = details;
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'Não autorizado') {
    super(message, 401, 'UNAUTHORIZED');
  }
}

// Uso:
app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await UserService.findById(req.params.id);
  if (!user) throw new NotFoundError('Usuário');
  res.json(user);
}));

// Handler global:
app.use((err, req, res, next) => {
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      error: err.message,
      code: err.code,
      ...(err.details && { details: err.details })
    });
  }
  // Erros inesperados
  console.error(err);
  res.status(500).json({ error: 'Erro interno' });
});

CORS

const cors = require('cors'); // npm install cors

// ---- Permitir tudo (desenvolvimento) ----
app.use(cors());

// ---- Configuração restrita (produção) ----
const corsOptions = {
  origin: (origin, callback) => {
    const allowed = [
      'https://meusite.com',
      'https://app.meusite.com',
      // null permite Postman/curl sem origin
      ...(process.env.NODE_ENV === 'development' ? ['http://localhost:3000', 'http://localhost:5173'] : [])
    ];
    if (!origin || allowed.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error(`CORS bloqueado para: ${origin}`));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-Id'],
  credentials: true,       // permite envio de cookies
  maxAge: 86400,           // cache de preflight por 24h
  optionsSuccessStatus: 200 // alguns browsers precisam de 200 no OPTIONS
};

app.use(cors(corsOptions));

// CORS por rota específica
app.get('/public', cors(), (req, res) => {
  res.json({ public: true });
});

Rate Limiting

const rateLimit = require('express-rate-limit'); // npm install express-rate-limit

// Rate limit global: 100 req por 15 min por IP
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 100,
  standardHeaders: true,    // retorna headers RateLimit-*
  legacyHeaders: false,      // desabilita X-RateLimit-*
  message: {
    error: 'Muitas requisições, tente novamente em 15 minutos'
  }
});
app.use(globalLimiter);

// Rate limit mais restrito para login/auth
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10, // máx 10 tentativas
  skipSuccessfulRequests: true, // não conta sucessos
  message: { error: 'Muitas tentativas de login' }
});
app.use('/api/auth/login', authLimiter);

// Rate limit com Redis (para múltiplos servidores)
const RedisStore = require('rate-limit-redis');
const redis = require('ioredis');

const limiterWithRedis = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  store: new RedisStore({
    sendCommand: (...args) => redis.call(...args)
  })
});

Helmet (Segurança)

const helmet = require('helmet'); // npm install helmet

// Uso básico — aplica todos os middlewares de segurança
app.use(helmet());

// Customizado:
app.use(helmet({
  // Content Security Policy
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net'],
      styleSrc: ["'self'", "'unsafe-inline'", 'fonts.googleapis.com'],
      imgSrc: ["'self'", 'data:', 'https:'],
      connectSrc: ["'self'", 'api.meusite.com'],
      fontSrc: ["'self'", 'fonts.gstatic.com'],
      objectSrc: ["'none'"],
      upgradeInsecureRequests: [],
    },
  },
  // HSTS: força HTTPS por 1 ano
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  },
  // Previne clickjacking
  frameguard: { action: 'deny' },
  // Esconde que é Express
  hidePoweredBy: true,
  // Previne sniffing de MIME type
  noSniff: true,
  // Previne XSS (legado, mas útil)
  xssFilter: true
}));

// CSP apenas para relatórios (não bloqueia)
app.use(helmet.contentSecurityPolicy({
  directives: { /* ... */ },
  reportOnly: true // apenas reporta, não bloqueia
}));

Upload de Arquivos com Multer

const multer = require('multer'); // npm install multer
const path = require('path');
const fs = require('fs');

// ---- Armazenamento em disco ----
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const uploadDir = path.join(__dirname, 'uploads');
    // Criar diretório se não existir
    if (!fs.existsSync(uploadDir)) {
      fs.mkdirSync(uploadDir, { recursive: true });
    }
    cb(null, uploadDir);
  },
  filename: (req, file, cb) => {
    // Evitar colisões: timestamp + nome original
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
    const ext = path.extname(file.originalname);
    cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
  }
});

// Filtro de tipo de arquivo
const fileFilter = (req, file, cb) => {
  const allowedTypes = /jpeg|jpg|png|gif|webp/;
  const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
  const mimetype = allowedTypes.test(file.mimetype);

  if (extname && mimetype) {
    cb(null, true); // aceitar
  } else {
    cb(new Error('Apenas imagens são permitidas (jpg, png, gif, webp)'));
  }
};

const upload = multer({
  storage,
  fileFilter,
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB máximo
    files: 5,                   // máximo 5 arquivos
  }
});

// ---- Rotas de upload ----

// Upload de arquivo único
app.post('/upload/avatar', upload.single('avatar'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'Nenhum arquivo enviado' });
  }
  res.json({
    filename: req.file.filename,
    size: req.file.size,
    mimetype: req.file.mimetype,
    url: `/uploads/${req.file.filename}`
  });
});

// Upload de múltiplos arquivos
app.post('/upload/gallery', upload.array('photos', 5), (req, res) => {
  if (!req.files?.length) {
    return res.status(400).json({ error: 'Nenhum arquivo enviado' });
  }
  const files = req.files.map(f => ({
    filename: f.filename,
    size: f.size,
    url: `/uploads/${f.filename}`
  }));
  res.json({ count: files.length, files });
});

// Upload de campos diferentes
app.post('/upload/document', upload.fields([
  { name: 'cover', maxCount: 1 },
  { name: 'attachments', maxCount: 3 }
]), (req, res) => {
  const { cover, attachments } = req.files;
  res.json({
    cover: cover?.[0]?.filename,
    attachments: attachments?.map(f => f.filename) || []
  });
});

// ---- Armazenamento em memória (para processar antes de salvar) ----
const uploadMemory = multer({ storage: multer.memoryStorage() });

app.post('/upload/process', uploadMemory.single('image'), async (req, res) => {
  // req.file.buffer tem os bytes da imagem
  // Pode usar sharp para redimensionar antes de salvar
  const sharp = require('sharp');
  const resized = await sharp(req.file.buffer)
    .resize(800, 600, { fit: 'inside' })
    .jpeg({ quality: 85 })
    .toBuffer();

  fs.writeFileSync(`uploads/processed-${Date.now()}.jpg`, resized);
  res.json({ ok: true });
});

Express com TypeScript

npm install express
npm install -D typescript @types/node @types/express ts-node-dev
# tsconfig.json mínimo
# npx tsc --init
// src/types/express.d.ts — extend Request
import { User } from '../models/User';

declare global {
  namespace Express {
    interface Request {
      user?: User;         // injetado pelo authMiddleware
      requestId?: string;  // injetado pelo requestIdMiddleware
    }
  }
}
// src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

export const authMiddleware = (
  req: Request,
  res: Response,
  next: NextFunction
): void => {
  const token = req.headers.authorization?.split(' ')[1];

  if (!token) {
    res.status(401).json({ error: 'Token não fornecido' });
    return;
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { id: string };
    req.user = decoded as any;
    next();
  } catch {
    res.status(401).json({ error: 'Token inválido' });
  }
};
// src/controllers/users.controller.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/users.service';

export class UsersController {
  constructor(private readonly userService: UserService) {}

  findAll = async (req: Request, res: Response, next: NextFunction) => {
    try {
      const users = await this.userService.findAll();
      res.json(users);
    } catch (err) {
      next(err);
    }
  };

  findById = async (req: Request, res: Response, next: NextFunction) => {
    try {
      const user = await this.userService.findById(req.params.id);
      if (!user) {
        res.status(404).json({ error: 'Usuário não encontrado' });
        return;
      }
      res.json(user);
    } catch (err) {
      next(err);
    }
  };

  create = async (req: Request, res: Response, next: NextFunction) => {
    try {
      const user = await this.userService.create(req.body);
      res.status(201).json(user);
    } catch (err) {
      next(err);
    }
  };
}
// src/routes/users.routes.ts
import { Router } from 'express';
import { UsersController } from '../controllers/users.controller';
import { UserService } from '../services/users.service';
import { authMiddleware } from '../middleware/auth.middleware';

const router = Router();
const controller = new UsersController(new UserService());

router.get('/', controller.findAll);
router.get('/:id', controller.findById);
router.post('/', authMiddleware, controller.create);

export default router;
// src/app.ts
import express, { Application } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import usersRouter from './routes/users.routes';
import { errorHandler } from './middleware/error.middleware';

export function createApp(): Application {
  const app = express();

  // Middlewares globais
  app.use(helmet());
  app.use(cors());
  app.use(morgan('dev'));
  app.use(express.json());
  app.use(express.urlencoded({ extended: true }));

  // Rotas
  app.use('/api/v1/users', usersRouter);

  // 404
  app.use((req, res) => {
    res.status(404).json({ error: 'Rota não encontrada' });
  });

  // Error handler (deve ser o último!)
  app.use(errorHandler);

  return app;
}
// src/server.ts
import { createApp } from './app';

const app = createApp();
const PORT = Number(process.env.PORT) || 3000;

app.listen(PORT, () => {
  console.log(`Servidor rodando na porta ${PORT}`);
});

// Graceful shutdown
process.on('SIGTERM', () => {
  console.log('SIGTERM recebido, encerrando...');
  process.exit(0);
});

Estrutura de Projeto Recomendada

meu-api/
├── src/
   ├── config/              # Configurações (db, redis, env validation)
   ├── database.ts
   └── env.ts
   ├── controllers/         # Handlers HTTP (entrada e saída)
   ├── users.controller.ts
   └── auth.controller.ts
   ├── middleware/          # Middlewares reutilizáveis
   ├── auth.middleware.ts
   ├── error.middleware.ts
   ├── validate.middleware.ts
   └── request-id.middleware.ts
   ├── models/              # Entidades / DTOs
   └── user.model.ts
   ├── routes/              # Definição de rotas
   ├── index.ts         # Agrega todos os routers
   ├── users.routes.ts
   └── auth.routes.ts
   ├── services/            # Lógica de negócio
   ├── users.service.ts
   └── auth.service.ts
   ├── repositories/        # Acesso ao banco de dados
   └── users.repository.ts
   ├── types/               # Tipagens globais
   └── express.d.ts
   ├── utils/               # Funções auxiliares
   ├── hash.ts
   └── jwt.ts
   ├── validators/          # Regras de validação
   └── users.validator.ts
   ├── app.ts               # Configuração do Express
   └── server.ts            # Ponto de entrada
├── tests/
   ├── unit/
   └── integration/
├── uploads/                 # Arquivos enviados (no .gitignore)
├── .env
├── .env.example
├── .gitignore
├── package.json
└── tsconfig.json

Padrão de resposta padronizado

// src/utils/api-response.ts
interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
  meta?: {
    page?: number;
    limit?: number;
    total?: number;
  };
}

export function success<T>(data: T, meta?: ApiResponse<T>['meta']): ApiResponse<T> {
  return { success: true, data, ...(meta && { meta }) };
}

export function failure(error: string): ApiResponse<never> {
  return { success: false, error };
}

// Uso nos controllers:
res.json(success(users, { page: 1, limit: 10, total: 42 }));
res.status(404).json(failure('Usuário não encontrado'));

Comparativo Express vs Fastify

CaracterísticaExpressFastify
Performance~50k req/s~70k req/s
ValidaçãoManual (express-validator)JSON Schema nativo
TypeScriptVia tipos externosNativo, inferência automática
Serializaçãores.json() normalSerialização otimizada por schema
PluginsMiddleware simplesSistema de plugins com encapsulamento
Curva de aprendizadoBaixaMédia
EcossistemaEnorme (maior do Node.js)Crescendo rápido
Async/awaitRequer wrapperNativo
LoggingMorgan (externo)Pino integrado
ComunidadeMuito grandeAtiva e crescendo
// Express — estilo tradicional
app.get('/users', async (req, res, next) => {
  try {
    const users = await db.query('SELECT * FROM users');
    res.json(users.rows);
  } catch (err) {
    next(err);
  }
});

// Fastify — mesmo endpoint
fastify.get('/users', {
  schema: {
    response: {
      200: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            id: { type: 'integer' },
            name: { type: 'string' }
          }
        }
      }
    }
  }
}, async (request, reply) => {
  const users = await db.query('SELECT * FROM users');
  return users.rows; // serialização automática e otimizada
});

Quando usar Express:

  • Projetos legados ou em equipes já familiarizadas
  • Quando o ecossistema de middlewares existente é crítico
  • Prototipagem rápida sem necessidade de performance extrema

Quando usar Fastify:

  • APIs de alta performance (microserviços, edge functions)
  • Projetos novos com TypeScript
  • Quando validação e serialização por schema são importantes

Dicas e Boas Práticas

// 1. Nunca exponha stack traces em produção
app.use((err, req, res, next) => {
  const isDev = process.env.NODE_ENV === 'development';
  res.status(err.status || 500).json({
    error: err.message,
    ...(isDev && { stack: err.stack })
  });
});

// 2. Sempre valide e sanitize entradas
const xss = require('xss');
app.use((req, res, next) => {
  if (req.body) {
    req.body = JSON.parse(xss(JSON.stringify(req.body)));
  }
  next();
});

// 3. Request ID para rastreabilidade
const { v4: uuidv4 } = require('uuid');
app.use((req, res, next) => {
  req.id = req.headers['x-request-id'] || uuidv4();
  res.set('X-Request-Id', req.id);
  next();
});

// 4. Graceful shutdown
const server = app.listen(PORT);

process.on('SIGTERM', async () => {
  console.log('Encerrando servidor...');
  server.close(async () => {
    await db.end(); // fechar conexão com banco
    process.exit(0);
  });
  // Force exit após 10s
  setTimeout(() => process.exit(1), 10000);
});

// 5. Trust proxy (quando atrás de nginx/load balancer)
app.set('trust proxy', 1); // req.ip retorna IP real do cliente

// 6. Limite de tamanho do body
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ limit: '10mb', extended: true }));