Express com TypeScript: Fundamentos de roteamento e ciclo requisição-resposta

Capítulo 5

Tempo estimado de leitura: 8 minutos

+ Exercício

O ciclo requisição-resposta no Express

No Express, cada requisição HTTP percorre uma cadeia previsível: middlewares (pré-processamento), roteamento (seleção da rota), handlers (a lógica da rota) e, por fim, a resposta. Entender esse fluxo é essencial para organizar o back-end, aplicar validações, autenticação, logs e padronizar respostas.

  • Middleware: função que recebe (req, res, next) e pode ler/modificar a requisição, encerrar a resposta, ou delegar para o próximo passo com next().
  • Router: “sub-aplicação” com rotas agrupadas (ex.: /api/v1, /health).
  • Handler: função final da rota (ou uma das funções) que normalmente produz a resposta.
  • Error middleware: middleware especial com assinatura (err, req, res, next), executado quando ocorre erro ou quando next(err) é chamado.

Ordem importa

O Express executa middlewares e rotas na ordem em que você registra com app.use e app.METHOD. Isso afeta desde parsing de JSON até autenticação e tratamento de 404.

Projeto base: Express + TypeScript com roteamento organizado

Estrutura sugerida

Uma organização simples e escalável para rotas e middlewares:

src/  app.ts  server.ts  routes/    index.ts    v1/      index.ts      health.routes.ts      echo.routes.ts  middlewares/    requestId.ts    notFound.ts    errorHandler.ts  types/    express.d.ts

Dependências

Instale Express e tipos:

npm i express npm i -D @types/express ts-node-dev

Observação: a configuração de TypeScript (tsconfig, build, etc.) já foi tratada anteriormente; aqui vamos focar no fluxo Express e tipagem.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

Implementando o app e o servidor

src/app.ts: composição do pipeline

O app concentra middlewares globais e monta routers por prefixo.

import express, { type Request, type Response } from "express";import { routes } from "./routes";import { requestId } from "./middlewares/requestId";import { notFound } from "./middlewares/notFound";import { errorHandler } from "./middlewares/errorHandler";export const app = express();app.disable("x-powered-by");app.use(express.json());app.use(requestId);app.get("/", (_req: Request, res: Response) => {  res.status(200).json({ ok: true, service: "api" });});app.use(routes);app.use(notFound);app.use(errorHandler);

src/server.ts: inicialização

import { app } from "./app";const port = Number(process.env.PORT ?? 3000);app.listen(port, () => {  console.log(`Listening on http://localhost:${port}`);});

Middlewares: antes, durante e depois do handler

Middleware de correlação: requestId

Um padrão útil é atribuir um ID por requisição e devolvê-lo em header para rastreamento.

import { randomUUID } from "node:crypto";import type { Request, Response, NextFunction } from "express";export function requestId(req: Request, res: Response, next: NextFunction) {  const id = req.header("x-request-id") ?? randomUUID();  res.setHeader("x-request-id", id);  (req as any).requestId = id;  next();}

Mais adiante, vamos tipar isso corretamente com module augmentation para evitar any.

Middleware de 404: notFound

Se nenhuma rota respondeu, este middleware finaliza com 404.

import type { Request, Response, NextFunction } from "express";export function notFound(req: Request, res: Response, _next: NextFunction) {  res.status(404).json({    error: "NOT_FOUND",    message: `No route for ${req.method} ${req.path}`,  });}

Error handler: errorHandler

Centraliza erros e padroniza respostas. A assinatura com 4 parâmetros é obrigatória para o Express reconhecer como middleware de erro.

import type { Request, Response, NextFunction } from "express";export function errorHandler(err: unknown, _req: Request, res: Response, _next: NextFunction) {  const message = err instanceof Error ? err.message : "Unexpected error";  res.status(500).json({ error: "INTERNAL_ERROR", message });}

Roteadores e padrões consistentes (prefixos, composição e versionamento)

Router raiz: src/routes/index.ts

Este arquivo monta versões e grupos de rotas. Um padrão comum é /api/v1 para permitir evolução sem quebrar clientes.

import { Router } from "express";import { v1Routes } from "./v1";export const routes = Router();routes.use("/api/v1", v1Routes);

Router da versão: src/routes/v1/index.ts

import { Router } from "express";import { healthRoutes } from "./health.routes";import { echoRoutes } from "./echo.routes";export const v1Routes = Router();v1Routes.use("/health", healthRoutes);v1Routes.use("/echo", echoRoutes);

Rotas de healthcheck: health.routes.ts

import { Router, type Request, type Response } from "express";export const healthRoutes = Router();healthRoutes.get("/", (_req: Request, res: Response) => {  res.status(200).json({ status: "up" });});

Note que o endpoint final fica GET /api/v1/health por composição de prefixos.

Tipagem no Express: Request, Response e NextFunction

Por que tipar?

Sem tipagem, é fácil acessar req.params, req.query e req.body assumindo formatos incorretos. Com TypeScript, você consegue:

  • Autocompletar e validação estática do formato esperado.
  • Evitar undefined e conversões implícitas.
  • Padronizar contratos de entrada/saída.

Genéricos do Request

O tipo Request do Express aceita genéricos para tipar parâmetros, resposta, body e query. A forma mais comum é:

Request<Params, ResBody, ReqBody, ReqQuery>

Na prática, você frequentemente tipa Params, ReqBody e ReqQuery.

Tipando params com segurança

Exemplo: rota que recebe :name e devolve em header e body.

import { Router, type Request, type Response } from "express";export const echoRoutes = Router();type EchoParams = { name: string };echoRoutes.get("/:name", (req: Request<EchoParams>, res: Response) => {  const { name } = req.params;  res.setHeader("x-echo-name", name);  res.status(200).json({ echoed: name });});

Tipando query (e lidando com string vs number)

Query string chega como string (ou array de strings). Mesmo que você espere um número, você deve converter e validar.

type EchoQuery = { times?: string };echoRoutes.get("/:name/repeat", (req: Request<{ name: string }, any, any, EchoQuery>, res: Response) => {  const timesRaw = req.query.times ?? "1";  const times = Number(timesRaw);  if (!Number.isFinite(times) || times < 1 || times > 5) {    return res.status(400).json({      error: "INVALID_QUERY",      message: "times must be a number between 1 and 5",    });  }  const result = Array.from({ length: times }, () => req.params.name);  res.status(200).json({ result });});

Repare no padrão: tipar para saber o que pode existir e validar para garantir que o valor é aceitável.

Tipando body (com validação mínima)

O Express não valida JSON automaticamente; ele apenas faz parsing. Você deve validar o formato esperado.

type EchoBody = { message: string };echoRoutes.post("/", (req: Request<{}, any, EchoBody>, res: Response) => {  const { message } = req.body;  if (typeof message !== "string" || message.trim().length === 0) {    return res.status(422).json({      error: "INVALID_BODY",      message: "message is required",    });  }  res.status(201).json({ received: message });});

Tipando propriedades adicionadas ao Request (module augmentation)

Se você adiciona requestId no middleware, o TypeScript não sabe disso por padrão. A forma correta é estender a interface Express.Request via declaration merging.

src/types/express.d.ts

declare global {  namespace Express {    interface Request {      requestId?: string;    }  }}export {};

Agora você pode usar req.requestId sem any:

echoRoutes.get("/:name", (req, res) => {  res.status(200).json({ echoed: req.params.name, requestId: req.requestId });});

Handlers assíncronos e propagação de erros

Em handlers async, erros devem chegar ao error handler. Em versões modernas do Express (5.x), erros lançados em async são encaminhados automaticamente; em Express 4.x, é comum usar try/catch e next(err) (ou um wrapper). Para manter o padrão explícito:

import type { Request, Response, NextFunction } from "express";echoRoutes.get("/async/fail", async (_req: Request, _res: Response, next: NextFunction) => {  try {    await Promise.reject(new Error("Boom"));  } catch (err) {    next(err);  }});

Mini-laboratório: observando headers, status codes e comportamento do Express

Objetivo: executar chamadas e observar como middlewares, roteamento e respostas se comportam em cenários diferentes. Use curl (ou qualquer cliente HTTP) e compare status, headers e body.

1) Verificando headers padrão e o x-request-id

Faça uma requisição e inspecione headers:

curl -i http://localhost:3000/api/v1/health

O que observar:

  • HTTP/1.1 200 OK
  • Header x-request-id retornado pelo middleware
  • Ausência de x-powered-by (desabilitado no app)

2) Enviando um x-request-id e verificando se ele é preservado

curl -i -H "x-request-id: lab-123" http://localhost:3000/api/v1/health

O que observar:

  • O header de resposta x-request-id deve ser lab-123

3) Testando 404 (rota inexistente) e formato de erro

curl -i http://localhost:3000/api/v1/does-not-exist

O que observar:

  • Status 404
  • Body JSON com error e message
  • O 404 acontece porque nenhuma rota respondeu e o middleware notFound finalizou a resposta

4) Testando validação de query e status code 400

curl -i "http://localhost:3000/api/v1/echo/ana/repeat?times=999"

O que observar:

  • Status 400
  • Mensagem indicando regra de validação

5) Testando body JSON e status code 201 vs 422

Body válido:

curl -i -X POST http://localhost:3000/api/v1/echo -H "content-type: application/json" -d '{"message":"oi"}'

Body inválido:

curl -i -X POST http://localhost:3000/api/v1/echo -H "content-type: application/json" -d '{"message":""}'

O que observar:

  • 201 quando o body atende ao contrato
  • 422 quando o formato é válido (JSON), mas o conteúdo não atende às regras

6) Forçando erro e verificando o error handler (500)

curl -i http://localhost:3000/api/v1/echo/async/fail

O que observar:

  • Status 500
  • Body padronizado pelo errorHandler
  • O fluxo: handler chama next(err) e o Express pula para o middleware de erro

Tabela rápida: onde tipar e o que validar

EntradaOnde ficaComo tiparValidações comuns
Parâmetros de rotareq.paramsRequest<{ id: string }>presença, formato (UUID), conversão para number quando necessário
Query stringreq.queryRequest<{}, any, any, { page?: string }>conversão, limites, valores permitidos
Body JSONreq.bodyRequest<{}, any, { name: string }>campos obrigatórios, tipos, regras de negócio
Headersreq.header()tipagem indireta (string)presença, normalização, fallback

Agora responda o exercício sobre o conteúdo:

Em um app Express com middlewares registrados em sequência, qual é o efeito de chamar next(err) dentro de um handler?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Ao chamar next(err), o Express pula o fluxo padrão e direciona para o error middleware (com 4 parâmetros), onde o erro pode ser tratado e a resposta padronizada (ex.: 500).

Próximo capitúlo

Express com TypeScript: Middlewares essenciais e composição de pipeline

Arrow Right Icon
Capa do Ebook gratuito Node.js Essencial: Construindo um Back-end com Express e TypeScript
31%

Node.js Essencial: Construindo um Back-end com Express e TypeScript

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.