Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA

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.

13 lições em trilha14 artigos no guiaAtualizado em agosto de 2026

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.

text
requisição
  -> parser de JSON
    -> logger
      -> autenticação
        -> validação
          -> controller
            -> middleware de erro
              -> resposta

Entender 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:

js
// 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:

bash
mkdir api-express
cd api-express
npm init -y
npm install express@5.2.1

No package.json:

json
{
  "type": "module",
  "scripts": {
    "dev": "node --watch src/server.js",
    "start": "node src/server.js"
  }
}

Agora src/server.js:

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:

bash
npm run dev
curl -i http://localhost:3000/health
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8

{“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:

js
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.params identifica partes dinâmicas do caminho;
  • request.query ajusta uma consulta com filtro, página e ordenação;
  • request.body carrega a representação enviada em POST ou PATCH.
js
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:

js
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:

js
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:

js
app.use(logger);                     // toda a aplicação
app.use('/admin', authenticate);     // grupo de caminhos
app.delete('/books/:id', authorize, deleteBook); // uma rota

O 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:

js
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:

text
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 servidor

Nã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:

js
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:

js
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:

js
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:

text
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.js

Controller 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:

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

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

js
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:

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

text
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ção

O 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, secure e sameSite adequados;
  • 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:

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

js
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:

js
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:

js
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:

  1. primeira rota e ciclo request/response;
  2. recurso, verbo e status HTTP;
  3. params, query, body e parser;
  4. middleware e ordem;
  5. Router por recurso;
  6. schema de validação;
  7. controller, serviço e repositório;
  8. erros esperados e inesperados;
  9. autenticação, autorização e CORS;
  10. teste com banco isolado;
  11. 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.

Ver a trilha
  1. 01Métodos HTTP e status code: 200, 201, 400, 404 e 500
  2. 02O que é uma API REST: recurso, verbo, status e contrato
  3. 03Express do zero: instalar, subir o servidor e criar a rota
  4. 04req.params, req.query e req.body no Express: qual usar
  5. 05Middleware no Express: next(), ordem e escopo de rota
  6. 06express.Router: dividir a API em arquivos por recurso
  7. 07CORS no Express: liberar o front sem abrir a API inteira
  8. 08Tratamento de erro centralizado no Express, num handler só
  9. 09Validar a entrada da API no Express com Zod
  10. 10Upload de arquivo no Express com multer: imagem e limite
  11. 11Hash de senha com bcrypt no Node: salvar e comparar
  12. 12Autenticação com JWT no Express: login e rota protegida
  13. 13Testar rota de API com node:test e supertest, sem Jest

express

Perguntas frequentes

Express ainda vale a pena em 2026?
Sim. Express 5 é estável, tem ecossistema enorme e torna HTTP visível para quem está aprendendo. Ele não impõe uma arquitetura completa, então a equipe precisa definir organização, validação, segurança e observação.
Preciso aprender o módulo http antes de Express?
Não precisa dominar, mas criar uma rota pequena com node:http ajuda a perceber o trabalho que o Express organiza. Entender método, URL, header, body e status é mais importante do que memorizar a API de baixo nível.
Qual é a diferença entre Express e NestJS?
Express é uma camada mínima de rotas e middlewares. NestJS é um framework opinativo com módulos, controllers, providers e injeção de dependência; por padrão ele pode usar Express por baixo. Comece pelo nível de estrutura que o tamanho e a equipe do projeto exigem.
Express 5 captura erros de funções async?
Sim. Quando um handler async rejeita ou lança erro, o Express 5 encaminha esse erro ao middleware de erro. Ainda é necessário traduzir erros esperados, não enviar stack ao cliente e cuidar de callbacks que falham fora da Promise retornada.
Express valida o JSON recebido?
Não. express.json transforma bytes em um valor JavaScript, mas não garante campos, tipos ou regras. Use um schema de runtime e trate a entrada como não confiável antes de chegar à regra ou ao banco.
Posso usar Express com TypeScript e Prisma?
Sim. Express oferece tipos, e Prisma gera um client tipado. TypeScript não valida dados externos em runtime, então a combinação ainda precisa de schemas para body, params, query e respostas de serviços externos.

Fontes consultadas

  1. Express — Installing — expressjs.com
  2. Express — Routing — expressjs.com
  3. Express — Using middleware — expressjs.com
  4. Express — Error handling — expressjs.com
  5. Express 5 — Moving to Express 5 — expressjs.com
  6. Express — Security best practices — expressjs.com
  7. Express — Performance best practices — expressjs.com