Criar REST API com Express no Node.js
1. Visão geral
Este guia o orienta a construir uma REST API de gerenciamento de Order com Express — desde a inicialização do projeto, definição de rotas, middleware, validação de dados, até tratamento centralizado de erros. Após ler, você será capaz de construir uma API CRUD completa, com testes automatizados usando Jest e Supertest, e conhecerá os erros comuns ao trabalhar com Express.
Exemplo ao longo do guia: uma API de gerenciamento de pedidos (Order) de um Customer, com suporte para criar, visualizar, editar e deletar.
2. Preparar o projeto
Crie um novo projeto Node.js e instale Express:
mkdir order-api && cd order-api
npm init -y
npm install express
npm install --save-dev jest supertest
Adicione type: module para usar a sintaxe import/export (ES Module) e declare o script de teste em package.json:
{
"name": "order-api",
"type": "module",
"scripts": {
"start": "node src/server.js",
"test": "node --experimental-vm-modules node_modules/.bin/jest"
}
}
Nota: Jest com ES Module requer a flag
--experimental-vm-modules. Se quiser evitar complicações, você pode remover"type": "module"e usarrequire/module.exports(CommonJS) — ambas são válidas, depende da preferência do time.
3. Criar servidor Express básico
// src/app.js
import express from 'express';
const app = express();
app.use(express.json()); // faz parse do body JSON em req.body
app.get('/health', (req, res) => {
res.json({ status: 'UP' });
});
export default app;
// src/server.js
import app from './app.js';
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Order API está rodando na porta ${PORT}`);
});
express.json()é um middleware integrado que faz o parse automático deContent-Type: application/jsonem um objetoreq.body. Sem essa linha,req.bodyseráundefined.- Separar
app.jsdeserver.jspara que os testes não precisem abrir uma porta real — essa é uma prática importante para REST API Node.js.
Teste assim:
node src/server.js
curl http://localhost:3000/health
{"status":"UP"}
4. Definir o resource Order
Use um “banco de dados” em memória usando um array para focar na API, sem depender de um banco de dados real.
// src/orders/orderRepository.js
let orders = [];
let nextId = 1;
export function findAll() {
return orders;
}
export function findById(id) {
return orders.find((o) => o.id === id);
}
export function create(data) {
const order = { id: nextId++, ...data, status: 'PENDING' };
orders.push(order);
return order;
}
export function update(id, data) {
const index = orders.findIndex((o) => o.id === id);
if (index === -1) return null;
orders[index] = { ...orders[index], ...data };
return orders[index];
}
export function remove(id) {
const index = orders.findIndex((o) => o.id === id);
if (index === -1) return false;
orders.splice(index, 1);
return true;
}
Este repositório simula as operações CRUD que você verá em qualquer ORM (Sequelize, Prisma…), com a diferença de que armazena em RAM em vez de um banco de dados.
5. Roteamento com Express Router
Em vez de anexar todas as rotas ao app, use express.Router() para modularizar por recurso:
// src/orders/orderRouter.js
import { Router } from 'express';
import * as repo from './orderRepository.js';
const router = Router();
router.get('/', (req, res) => {
res.json(repo.findAll());
});
router.get('/:id', (req, res, next) => {
const order = repo.findById(Number(req.params.id));
if (!order) return next({ status: 404, message: 'Order não existe' });
res.json(order);
});
router.post('/', (req, res) => {
const order = repo.create(req.body);
res.status(201).json(order);
});
router.put('/:id', (req, res, next) => {
const order = repo.update(Number(req.params.id), req.body);
if (!order) return next({ status: 404, message: 'Order não existe' });
res.json(order);
});
router.delete('/:id', (req, res, next) => {
const deleted = repo.remove(Number(req.params.id));
if (!deleted) return next({ status: 404, message: 'Order não existe' });
res.status(204).end();
});
export default router;
Anexe o router ao app com o prefixo /orders:
// src/app.js (atualizado)
import orderRouter from './orders/orderRouter.js';
app.use('/orders', orderRouter);
router.get('/:id', ...)—:idé um parâmetro de rota, acessado viareq.params.id(sempre é string, precisaNumber()para comparação).- Chamar
next({ status: 404, message: ... })passa o erro para o middleware de tratamento de erros (seção 7), em vez de repetirres.status(404)...em cada rota. 204 No Contentpara DELETE bem-sucedido — sem retornar body.
6. Validar dados de entrada
Validar manualmente com if/else é fácil de pular casos. Use express-validator para ser conciso e claro:
npm install express-validator
// src/orders/orderValidator.js
import { body, validationResult } from 'express-validator';
export const validateCreateOrder = [
body('customerName').isString().notEmpty().withMessage('customerName é obrigatório'),
body('amount').isFloat({ gt: 0 }).withMessage('amount deve ser maior que 0'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
},
];
Anexe o validador à rota POST:
// src/orders/orderRouter.js (atualizado)
import { validateCreateOrder } from './orderValidator.js';
router.post('/', validateCreateOrder, (req, res) => {
const order = repo.create(req.body);
res.status(201).json(order);
});
- Cada
body(...)é uma regra que valida um campo; se falhar, erros são coletados emvalidationResult(req). - O middleware final no array verifica
errors.isEmpty()— se houver erros, retorna400com detalhes e não chamanext()para bloquear a requisição de chegar ao handler real.
Envie uma requisição sem amount:
curl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-d '{"customerName": "Nam"}'
{"errors":[{"type":"field","msg":"amount deve ser maior que 0","path":"amount",...}]}
7. Tratamento centralizado de erros
Express permite definir um middleware de tratamento de erro — é um middleware com 4 parâmetros (err, req, res, next), colocado após todas as rotas:
// src/errorHandler.js
export function errorHandler(err, req, res, next) {
const status = err.status || 500;
const message = err.message || 'Erro no sistema';
if (status === 500) {
console.error(err); // registre o erro real para debug, não retorne stack trace ao cliente
}
res.status(status).json({ message });
}
// src/app.js (atualizado, adicione no FINAL do arquivo)
import { errorHandler } from './errorHandler.js';
app.use(errorHandler);
- Express reconhece esse middleware como error handler pela quantidade de parâmetros (4), não pelo nome da função.
- Deve-se colocar
app.use(errorHandler)por último, após todos osapp.use(router)— Express processa middleware na ordem em que são declarados. - Qualquer
next(err)ou erro throw em uma rota (se usarexpress-async-errorsou try/catch) cairá aqui.
Erro comum: esquecer de chamar
next(err)em código async (por exemploawait db.query(...)throw) faz Express não capturar o erro, causando crash do servidor ou requisição pendente. Com Express 5 (versão mais recente), async handler com throw de erro é automaticamente capturado — mas Express 4 requer que você envolva em try/catch e chamenext(err)manualmente.
8. Adicionar camada Service
Quando a lógica fica mais complexa (por exemplo: não permitir criar order se amount ultrapassar o limite do cliente), não coloque tudo no router. Extraia para orderService:
// src/orders/orderService.js
import * as repo from './orderRepository.js';
const MAX_AMOUNT = 100_000_000;
export function createOrder(data) {
if (data.amount > MAX_AMOUNT) {
const err = new Error('Ultrapassou o limite permitido');
err.status = 422;
throw err;
}
return repo.create(data);
}
// src/orders/orderRouter.js (atualizado rota POST)
import * as orderService from './orderService.js';
router.post('/', validateCreateOrder, (req, res, next) => {
try {
const order = orderService.createOrder(req.body);
res.status(201).json(order);
} catch (err) {
next(err);
}
});
Agora o router cuida apenas de HTTP (status code, parse param), enquanto orderService cuida da lógica de negócio. Esse é um padrão similar à camada Controller–Service em outros frameworks, facilitando testar lógica de negócio independentemente de HTTP.
9. Escrever testes com Jest e Supertest
Supertest permite chamar a API diretamente no objeto app (instância Express) sem precisar abrir uma porta real — essa é a razão pela qual a seção 3 separa app.js de server.js.
// tests/order.test.js
import request from 'supertest';
import app from '../src/app.js';
describe('Order API', () => {
it('criar order com sucesso retorna 201 e id', async () => {
const response = await request(app)
.post('/orders')
.send({ customerName: 'Nam', amount: 500000 });
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
customerName: 'Nam',
amount: 500000,
status: 'PENDING',
});
expect(response.body.id).toBeDefined();
});
it('falta amount retorna 400', async () => {
const response = await request(app)
.post('/orders')
.send({ customerName: 'Nam' });
expect(response.status).toBe(400);
});
it('buscar order inexistente retorna 404', async () => {
const response = await request(app).get('/orders/9999');
expect(response.status).toBe(404);
expect(response.body.message).toBe('Order não existe');
});
});
Execute os testes:
npm test
PASS tests/order.test.js
Order API
✓ criar order com sucesso retorna 201 e id
✓ falta amount retorna 400
✓ buscar order inexistente retorna 404
request(app)inicializa um cliente HTTP virtual anexado diretamente ao app Express — mais rápido que testar pela rede real.toMatchObjectverifica apenas os campos especificados, ignorando os demais (como id gerado automaticamente) — útil para testar responses com valores dinâmicos.
Nota: porque
ordersé um array em memória compartilhado entre os testes, testes executados em ordem podem se afetar (por exemplo, a order criada no teste 1 ainda existe quando o teste 2 roda). Com uma API real conectada a um banco de dados, resete os dados embeforeEachou use transaction rollback.
10. Quando não usar Express puro
- API grande, múltiplos domínios complexos: Express não tem convenções obrigatórias sobre estrutura, facilitando “spaghetti code” se o time não estabelecer regras próprias. Considere NestJS (com DI, módulos, decoradores como Spring) se o time está acostumado com arquitetura layered clara.
- Precisa de type-safety forte: Express + JavaScript puro não detecta erros de tipo em tempo de compilação. Se o projeto é grande, use TypeScript desde o início, não espere “crescer depois converter” — o custo de conversão posterior é muito maior.
- Alto throughput, precisa de streaming/WebSocket complexo: Express ainda funciona, mas Fastify ou frameworks com mais otimizações de desempenho (benchmark serialização JSON mais rápida) podem ser mais adequados para sistemas com tráfego extremamente grande.
Para API pequena–média com CRUD claro como no exemplo acima, Express continua sendo uma escolha rápida e concisa, com comunidade grande e muitos middlewares prontos (auth, rate-limit, CORS…).
11. Conclusão
Neste guia, você construiu uma REST API Order completa com Express: roteamento com Router, validação de entrada com express-validator, tratamento centralizado de erros com middleware de 4 parâmetros, e separação da lógica de negócio em camada service para facilitar testes. A seção de testes com Jest + Supertest o deixa confiante para refatorar sem medo de quebrar o comportamento da API. Dessa base, você pode adicionar conexão com banco de dados real (Prisma, Mongoose), autenticação (JWT), ou migrar para TypeScript conforme o projeto cresce.