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=supersegredoMiddleware 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 funcionaMiddleware
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.jsonPadrã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ística | Express | Fastify |
|---|---|---|
| Performance | ~50k req/s | ~70k req/s |
| Validação | Manual (express-validator) | JSON Schema nativo |
| TypeScript | Via tipos externos | Nativo, inferência automática |
| Serialização | res.json() normal | Serialização otimizada por schema |
| Plugins | Middleware simples | Sistema de plugins com encapsulamento |
| Curva de aprendizado | Baixa | Média |
| Ecossistema | Enorme (maior do Node.js) | Crescendo rápido |
| Async/await | Requer wrapper | Nativo |
| Logging | Morgan (externo) | Pino integrado |
| Comunidade | Muito grande | Ativa 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 }));