Guia completo
Express 5: guia completo para criar APIs com Node.js
Aprenda rotas, requisição, resposta, middleware, validação, erros, segurança, testes e deploy construindo uma API organizada com Express 5.
Express transforma o módulo HTTP do Node numa API que dá para ler e manter. Em vez de separar método, URL e corpo manualmente para cada requisição, você declara rotas, encaixa funções intermediárias e responde com status e JSON.
Neste guia, a gente vai montar o mapa completo do Express 5: primeira rota,
request, response, params, query, body, middleware, validação, erros,
organização, segurança, teste e produção. Cada assunto aponta para a lição que o
executa com mais detalhe; o guia explica a ordem e as decisões que ligam tudo.
Você precisa saber função, objeto, módulo e async/await. Se isso ainda estiver
solto, passe pela base de JavaScript e pelo guia de Node.
Express não cria outra linguagem: ele organiza
o JavaScript que recebe e responde HTTP.
A recepção de um prédio: o mapa mental do Express
Imagine a recepção de um prédio comercial. Cada visitante chega com um destino e um pedido. A recepção lê o endereço, confere identificação, registra horário, encaminha ao andar certo e devolve uma orientação.
No Express, a requisição é o visitante. Método e caminho formam o destino. As
funções de middleware são balcões de conferência. O handler da rota cuida
da tarefa específica. O objeto response leva status, headers e corpo de volta
ao cliente.
A comparação tem limite: Express não é servidor separado do Node. Ele é uma biblioteca executada no mesmo processo, apoiada nas APIs HTTP do runtime. Se um handler bloqueia a thread com uma conta pesada, a recepção inteira para.
O fluxo técnico é uma sequência de funções. Cada uma pode encerrar a resposta,
passar adiante com next ou produzir um erro. Ordem de registro é ordem de
execução.
requisição
-> parser de JSON
-> logger
-> autenticação
-> validação
-> controller
-> middleware de erro
-> respostaEntender essa fila resolve boa parte dos bugs de iniciante: req.body vazio,
requisição pendurada, headers enviados duas vezes e erro que nunca chega ao
tratador.
Express, node:http, Fastify ou NestJS?
node:http é a base sem framework. Ele ensina o protocolo e atende serviços
pequenos, mas você escreve roteamento, parsing e organização. Express adiciona
uma camada mínima e deixa quase todas as escolhas com você.
Fastify também é um framework HTTP e prioriza schema, plugins e desempenho. NestJS entrega uma arquitetura maior, com módulos, controllers, providers e injeção de dependência; normalmente usa Express como adapter padrão, embora possa usar Fastify.
Pense em ferramentas de oficina. A chave avulsa dá controle e exige mais mão de obra; uma bancada organizada acelera tarefas comuns; uma linha de montagem impõe posições e processo. Nenhuma é “a melhor” fora do tamanho do trabalho.
Express é uma boa primeira escolha porque deixa método, rota, middleware e resposta à vista. Equipes grandes precisam complementar essa liberdade com convenções. Se cada pessoa inventar sua pasta e formato de erro, o framework não vai impedir.
Express 5 não é só Express 4 com número novo
Projetos e cursos antigos ainda mostram Express 4. A maior mudança percebida no
dia a dia é que handlers e middlewares que devolvem uma Promise rejeitada têm o
erro encaminhado automaticamente. Isso remove muito try/catch cujo único
trabalho era chamar next(error), mas não remove o middleware de erro nem a
necessidade de classificar falhas.
Também houve mudanças de assinatura, parsing de caminho, defaults e métodos
removidos. Por exemplo, formas antigas de response.send que misturavam body e
status não devem ser copiadas:
// contrato atual, explícito
response.status(201).send({ id: 42 });
response.sendStatus(204);Uma migração saudável começa com testes do contrato HTTP. Atualize dependência, execute a suíte, leia cada depreciação e procure padrões removidos. Não faça uma troca de major junto com reorganização total das rotas; se o comportamento mudar, você precisa saber qual decisão causou.
O mínimo de Node também mudou. Express 5 exige Node 18 ou superior, e este blog
usa uma linha LTS atual. Fixe engines, versão de CI e imagem de contêiner para
que produção não descubra incompatibilidade depois do merge.
Se um tutorial instala express sem mostrar versão, confirme o package-lock.
Sintaxe que funciona em um vídeo pode ter sido executada em outra major. O nome
correto da versão faz parte do exemplo, não é nota de rodapé.
Instalação e a menor API que responde
Crie um projeto ESM:
mkdir api-express
cd api-express
npm init -y
npm install express@5.2.1No package.json:
{
"type": "module",
"scripts": {
"dev": "node --watch src/server.js",
"start": "node src/server.js"
}
}Agora src/server.js:
import express from 'express';
const app = express();
app.get('/health', (_request, response) => {
response.json({ status: 'ok' });
});
app.listen(3000, '0.0.0.0', () => {
console.log('API em http://localhost:3000');
});Execute e observe:
npm run dev
curl -i http://localhost:3000/health{“status”:“ok”}
app.get combina método GET e caminho /health. response.json serializa o
objeto, define o tipo e encerra a resposta. A lição primeira rota com Express
faz esse ciclo linha por linha e reproduz porta ocupada.
Rota é contrato de recurso, não nome de função
Uma API de livros pode declarar:
app.get('/books', listBooks);
app.get('/books/:id', getBook);
app.post('/books', createBook);
app.patch('/books/:id', updateBook);
app.delete('/books/:id', deleteBook);O substantivo fica no caminho; o verbo HTTP descreve a intenção. Use plural
consistente e hierarquia apenas quando há relação real. /users/:userId/orders
pode representar pedidos daquele usuário; /getAllBooks repete o verbo e
empobrece o contrato.
Ordem importa quando rotas podem colidir. Registre /books/search antes de
/books/:id, ou valide id de modo que a palavra search não seja interpretada
como identificador.
Params, query e body ocupam lugares diferentes
Três entradas chegam pela requisição:
request.paramsidentifica partes dinâmicas do caminho;request.queryajusta uma consulta com filtro, página e ordenação;request.bodycarrega a representação enviada em POST ou PATCH.
app.get('/books/:id', (request, response) => {
console.log(request.params.id); // "42"
console.log(request.query.include); // "reviews"
response.sendStatus(204);
});O caminho /books/42?include=reviews identifica o livro 42 e pede uma variação
da resposta. Parâmetro de URL e query chegam como texto. TypeScript dizer
number não converte o valor recebido.
Para ler JSON, registre o parser antes das rotas:
app.use(express.json({ limit: '32kb' }));
app.post('/books', (request, response) => {
console.log(request.body);
response.status(201).json({ data: request.body });
});Sem express.json, request.body fica undefined. Com ele, o body vira um valor
JavaScript ainda não confiável. A lição de params, query e body
mostra entradas boas, ausentes e malformadas.
Middleware é uma fila, não uma pasta
Middleware é uma função com acesso a request, response e next. Ele pode
observar, acrescentar contexto, barrar ou passar adiante:
function requestId(request, response, next) {
request.id = crypto.randomUUID();
response.setHeader('x-request-id', request.id);
next();
}
app.use(requestId);Se esquecer next() e não responder, o cliente espera até timeout. Se chamar
next() depois de responder e a função seguinte também responder, aparece o
erro de headers enviados duas vezes.
Use escopo proporcional:
app.use(logger); // toda a aplicação
app.use('/admin', authenticate); // grupo de caminhos
app.delete('/books/:id', authorize, deleteBook); // uma rotaO middleware é explicado como uma fila de balcões,
com ordem, async e o bug de req.body vazio. Não coloque toda regra de negócio
em middleware. Ele serve bem a preocupações transversais; cálculo de preço e
política de estoque pertencem a serviços do domínio.
Validação acontece na fronteira
Trate params, query, body e header como entrada externa. Um schema de runtime pode validar e normalizar:
import { z } from 'zod';
const createBookSchema = z.object({
title: z.string().trim().min(2).max(120),
priceCents: z.number().int().nonnegative(),
});
app.post('/books', (request, response) => {
const input = createBookSchema.parse(request.body);
response.status(201).json({ data: input });
});O schema define o que entra na regra. Banco com coluna NOT NULL continua
necessário: validação da API melhora mensagem, restrição do banco protege os
dados de qualquer cliente.
Evite espalhar if (!title) em cada controller. Um contrato centralizado é
testável e pode produzir formato de erro consistente. Mas não devolva a estrutura
interna inteira da biblioteca sem decidir o que o cliente precisa ver.
Status code conta o que aconteceu
Uma família útil para começar:
200 OK -> leitura ou alteração com corpo
201 Created -> recurso criado; inclua Location
204 No Content -> sucesso sem corpo
400 Bad Request -> entrada inválida
401 Unauthorized -> falta autenticação válida
403 Forbidden -> autenticado sem permissão
404 Not Found -> recurso não existe
409 Conflict -> conflito com estado atual
500 Internal Error -> falha inesperada do servidorNão responda 200 com { error: true }. Ferramentas, cache, observação e front
usam o status como primeira camada do contrato. Também não transforme todo erro
em 500: id malformado e livro ausente são resultados conhecidos.
Uma criação correta:
const book = await repository.create(input);
response
.status(201)
.location(`/books/${book.id}`)
.json({ data: book });Erro esperado e erro inesperado seguem caminhos distintos
Express 5 encaminha uma Promise rejeitada ao middleware de erro:
app.get('/books/:id', async (request, response) => {
const book = await repository.findById(request.params.id);
if (!book) throw new NotFoundError('livro não encontrado');
response.json({ data: book });
});O tratador vem depois das rotas e tem quatro argumentos:
app.use((error, request, response, next) => {
if (error instanceof NotFoundError) {
return response.status(404).json({ error: error.message });
}
request.log?.error({ error }, 'request failed');
return response.status(500).json({ error: 'erro interno' });
});Stack trace fica no servidor. O cliente recebe mensagem estável e um request id para suporte. A lição de tratamento de erros no Express reproduz sync, async e resposta iniciada.
Callbacks que disparam depois da função retornar precisam propagar seu próprio erro. E depois que headers foram enviados, seu middleware pode precisar delegar ao tratador padrão em vez de tentar criar outra resposta.
Separe transporte, regra e dados
Projetos pequenos começam num arquivo; crescem melhor quando cada pasta responde uma pergunta:
src/
app.js # configura Express
server.js # abre porta e sinais
routes/
books.routes.js # método + caminho
controllers/
books.controller.js # traduz HTTP
services/
books.service.js # regra de negócio
repositories/
books.repository.js # persistência
schemas/
books.schema.js # entrada externa
errors/
app-error.jsController lê contrato HTTP e chama serviço. Serviço decide se pode vender sem estoque. Repositório conversa com Prisma ou SQL. Essa separação não é religião: ela permite testar regra sem fabricar request e trocar persistência sem ensinar SQL ao controller.
Não crie uma classe vazia para cada verbo apenas para parecer corporativo. Comece
com módulos por recurso e extraia uma camada quando existir uma responsabilidade
real. O roteador modular mostra como montar
Router sem esconder o caminho final.
Documente o contrato e evolua sem surpreender o cliente
Uma rota funciona para alguém: navegador, aplicativo, integração ou outro time. O contrato precisa declarar método, caminho, autenticação, parâmetros, body, respostas e exemplos. OpenAPI é um formato conhecido para isso; ele pode gerar documentação e clientes, mas não substitui teste.
Comece pelo comportamento que o cliente observa:
paths:
/books/{id}:
get:
summary: Busca um livro
parameters:
- in: path
name: id
required: true
schema: { type: integer, minimum: 1 }
responses:
"200": { description: Livro encontrado }
"400": { description: Identificador inválido }
"404": { description: Livro não encontrado }Mantenha schema e implementação próximos para reduzir divergência. Algumas
equipes geram OpenAPI a partir dos schemas; outras validam a especificação contra
testes. O método importa menos que detectar quando a página diz 200 e a API
responde 204.
Evolução compatível normalmente acrescenta campo opcional, rota ou valor que o
cliente sabe ignorar. Renomear campo, mudar significado, tornar opcional em
obrigatório ou remover valor pode quebrar consumidores. Antes de criar /v2
para qualquer ajuste, tente uma mudança aditiva e meça uso do comportamento
antigo.
Versão pode morar no caminho, header ou negociação; escolha uma política e diga como a versão anterior será descontinuada. O pior cenário é versão invisível em que o mesmo endpoint muda de significado numa sexta-feira.
Operações que podem ser repetidas merecem idempotência. GET, PUT e DELETE têm semântica idempotente no protocolo; POST de pagamento ou pedido pode receber uma chave criada pelo cliente. O servidor guarda resultado por chave e impede que um timeout seguido de nova tentativa cobre duas vezes.
app.post('/orders', idempotency, async (request, response) => {
const order = await service.create(request.body, request.idempotencyKey);
response.status(201).json({ data: order });
});O middleware não pode guardar apenas em memória se existem várias instâncias. Use armazenamento compartilhado, escopo por usuário, hash do pedido e expiração. Mesma chave com body diferente deve ser conflito, não atalho para resultado antigo.
Documente também limites: tamanho de body, paginação máxima, timeout e política de repetição. Cliente que sabe a regra se comporta melhor e reduz carga.
Autenticação identifica; autorização permite
Autenticação responde “quem é?”. Autorização responde “esta pessoa pode fazer isso?”. Um token válido não autoriza automaticamente apagar qualquer livro.
async function authenticate(request, response, next) {
const token = request.headers.authorization?.replace(/^Bearer /, '');
if (!token) return response.status(401).json({ error: 'não autenticado' });
request.user = await tokenService.verify(token);
next();
}
function requireRole(role) {
return (request, response, next) => {
if (!request.user.roles.includes(role)) {
return response.status(403).json({ error: 'sem permissão' });
}
next();
};
}Verificação real deve validar assinatura, expiração, emissor e audiência. Chave e algoritmo não vêm do próprio token sem uma política confiável. Para recurso de usuário, consulte pertencimento no serviço; papel global nem sempre basta.
Senha entra como texto, é validada, transformada com algoritmo de hash adequado e nunca volta na resposta. Token em log, URL ou erro vira vazamento.
CORS não protege sua API de outros servidores
CORS é uma política do navegador. Ele decide se JavaScript de uma origem pode
ler a resposta de outra. curl, aplicativo mobile e servidor não dependem dessa
barreira.
Configure origens conhecidas:
import cors from 'cors';
const allowed = new Set(['https://app.exemplo.com']);
app.use(cors({
origin(origin, callback) {
if (!origin || allowed.has(origin)) return callback(null, true);
return callback(new Error('origem não permitida'));
},
credentials: true,
}));* junto com credenciais não é configuração válida. E liberar CORS não resolve
401: autenticação continua separada. A lição de CORS no Express
mostra preflight e a diferença entre origem e permissão.
Upload é entrada grande e potencialmente hostil
Upload exige limite de tamanho, tipo permitido, nome gerado pelo servidor, armazenamento fora da pasta pública e, conforme o risco, varredura.
Não confie em extensão ou Content-Type enviado pelo cliente. Nunca use o nome
original diretamente como caminho. Em escala, a API costuma autorizar upload
direto para armazenamento de objetos e guardar apenas metadados.
cliente -> pede autorização à API
API -> devolve URL temporária com limite
cliente -> envia ao armazenamento
armazenamento -> evento de processamento
API -> marca arquivo como disponível após validaçãoO artigo upload de arquivo com Express constrói o fluxo local e reproduz formato e tamanho inválidos.
Segurança é uma coleção de limites
Comece com estes controles:
- use TLS no proxy ou plataforma;
- atualize Node, Express e dependências;
- limite body, upload, tempo e quantidade de requisições;
- valide toda entrada e serialize apenas campos públicos;
- use queries parametrizadas e usuário de banco com pouco privilégio;
- configure headers de segurança conforme o produto;
- proteja cookie com
httpOnly,secureesameSiteadequados; - não revele versão, stack, caminho local ou segredo;
- registre ações sensíveis sem registrar credenciais;
- desligue com graça e mantenha backup testado.
Rate limit tem chave e escopo. Limitar somente IP pode punir uma empresa inteira atrás do mesmo proxy; limitar somente usuário não barra tentativa antes do login. Combine sinais e guarde contador num serviço compartilhado quando houver várias instâncias.
Se Express estiver atrás de proxy, configure trust proxy com a topologia real.
Aceitar qualquer header encaminhado permite falsificar IP e protocolo.
Teste a API por fora e a regra por dentro
Uma função de negócio recebe teste unitário sem Express. A API recebe teste de integração que percorre parser, rota, middleware e status:
import test from 'node:test';
import assert from 'node:assert/strict';
import request from 'supertest';
import { app } from '../src/app.js';
test('POST /books rejeita título curto', async () => {
const response = await request(app)
.post('/books')
.send({ title: 'A', priceCents: 1000 });
assert.equal(response.status, 400);
assert.equal(response.body.error, 'dados inválidos');
});Exporte app sem chamar listen. server.js fica responsável por porta. Assim
o teste não disputa endereço e a mesma aplicação entra no runtime.
Teste sucesso, entrada inválida, recurso ausente, permissão e falha de dependência. Banco de integração deve ser isolado e receber migrações do mesmo histórico. Mock de repositório ajuda a testar serviço, mas não substitui pelo menos um caminho com banco real.
Performance começa no banco, não em trocar framework
Antes de otimizar, meça percentis de latência, taxa de erro, event loop, pool e consulta. Uma API gasta frequentemente mais tempo esperando banco e serviço externo que executando o roteador.
Evite trabalho síncrono pesado no handler. Paginação impede resposta gigante. Compressão pode ficar no proxy. Cache precisa de chave, expiração e invalidação, não apenas um objeto global que cresce.
app.get('/books', async (request, response) => {
const page = Math.max(Number(request.query.page) || 1, 1);
const take = Math.min(Math.max(Number(request.query.limit) || 20, 1), 100);
const skip = (page - 1) * take;
const result = await service.list({ skip, take });
response.json({ data: result.items, page, total: result.total });
});Em lista muito profunda, cursor pode ser melhor que offset. O contrato deve
dizer ordem estável e como pedir a próxima página.
Resposta grande pode ser stream, mas erro muda de lugar
Express permite transmitir arquivo ou relatório sem carregar tudo na memória. Um stream trabalha em pedaços e respeita backpressure do destino:
app.get('/exports/books.csv', async (_request, response, next) => {
response.type('text/csv');
response.attachment('books.csv');
const stream = service.createCsvStream();
stream.on('error', next);
stream.pipe(response);
});O ganho é memória previsível. O custo é que um erro depois de enviar headers não
pode virar um JSON 500 normal. O cliente pode receber arquivo incompleto e a
aplicação precisa registrar falha. Para exportações longas, muitas arquiteturas
criam um job, guardam o arquivo e notificam quando estiver pronto.
Arquivo estático pode ser servido por express.static, mas produção costuma
colocar assets num CDN ou proxy, com cache e compressão. Use Express quando a
autorização ou geração depende da aplicação; não transforme cada imagem pública
numa requisição cara ao Node sem motivo.
Streaming não corrige consulta que tenta ler milhões de linhas de uma vez. A fonte também precisa paginar ou usar cursor; caso contrário só transferimos o estouro de memória para o repositório.
Produção: porta, proxy, sinais e observação
Leia porta e configuração do ambiente. Escute 0.0.0.0 dentro do contêiner e
trate sinais para parar de aceitar requisições antes de fechar recursos:
const server = app.listen(process.env.PORT ?? 3000, '0.0.0.0');
process.on('SIGTERM', () => {
server.close(async () => {
await database.disconnect();
process.exit(0);
});
});Use proxy para TLS e limite externo. Healthcheck de vivacidade confirma processo; prontidão confirma dependências necessárias. Um banco momentaneamente lento não deve provocar reinício infinito sem entender a plataforma.
Log útil contém horário, request id, método, rota normalizada, status, duração e identidade quando permitido. Não inclua senha, token e body sensível. Métrica mostra tendência; trace liga a chamada ao banco e serviços; alerta aponta impacto para usuário.
O tutorial API Node com TypeScript, Prisma, PostgreSQL e Docker junta essas peças num projeto executado de ponta a ponta.
A trilha Express em uma ordem que evita adivinhação
Estude e entregue nesta sequência:
- primeira rota e ciclo request/response;
- recurso, verbo e status HTTP;
- params, query, body e parser;
- middleware e ordem;
- Router por recurso;
- schema de validação;
- controller, serviço e repositório;
- erros esperados e inesperados;
- autenticação, autorização e CORS;
- teste com banco isolado;
- logs, métricas, segurança e deploy.
A missão deste guia é criar uma API de tarefas com usuário. Cada pessoa enxerga
e altera somente as próprias tarefas. O contrato deve ter 201, 204, 400,
401, 403, 404 e 409 em cenários realmente reproduzidos.
O critério final não é quantidade de rotas. Outra pessoa deve clonar, configurar o ambiente, aplicar migração, executar testes e receber logs suficientes para diagnosticar um erro sem abrir o código. Se você consegue explicar por qual middleware a requisição passou e quem tomou cada decisão, Express deixou de ser uma sequência de callbacks e virou arquitetura consciente.
Trilha
Node e APIs
JavaScript no servidor: rotas, banco de dados, autenticação e a API que o front consome.
- 01Métodos HTTP e status code: 200, 201, 400, 404 e 500
- 02O que é uma API REST: recurso, verbo, status e contrato
- 03Express do zero: instalar, subir o servidor e criar a rota
- 04req.params, req.query e req.body no Express: qual usar
- 05Middleware no Express: next(), ordem e escopo de rota
- 06express.Router: dividir a API em arquivos por recurso
- 07CORS no Express: liberar o front sem abrir a API inteira
- 08Tratamento de erro centralizado no Express, num handler só
- 09Validar a entrada da API no Express com Zod
- 10Upload de arquivo no Express com multer: imagem e limite
- 11Hash de senha com bcrypt no Node: salvar e comparar
- 12Autenticação com JWT no Express: login e rota protegida
- 13Testar rota de API com node:test e supertest, sem Jest
express
Cannot set headers after they are sent to the client
Perguntas frequentes
Express ainda vale a pena em 2026?
Preciso aprender o módulo http antes de Express?
Qual é a diferença entre Express e NestJS?
Express 5 captura erros de funções async?
Express valida o JSON recebido?
Posso usar Express com TypeScript e Prisma?
Fontes consultadas
- Express — Installing — expressjs.com
- Express — Routing — expressjs.com
- Express — Using middleware — expressjs.com
- Express — Error handling — expressjs.com
- Express 5 — Moving to Express 5 — expressjs.com
- Express — Security best practices — expressjs.com
- Express — Performance best practices — expressjs.com
