Express do zero: instalar, subir o servidor e criar a rota
Instalar o Express, criar o app, responder na primeira rota e entender o que o app.listen faz, com o servidor subindo no terminal de verdade.
O Express é uma camada fina em cima do servidor HTTP do Node: ele guarda uma
lista de rotas e chama a sua função quando a requisição bate no caminho certo.
Três linhas põem uma API no ar — criar o app, registrar uma rota e chamar
app.listen.
O projeto desta lição é a API da Padaria da Esquina: ela precisa publicar o cardápio do dia e receber encomendas pelo site. Tudo que aparece dentro de um bloco de saída aqui saiu do terminal de uma máquina real, um MacBook rodando Node 24.16.0 e Express 5.2.1.
Uma recepção com uma lista de endereços
Pense na API como a recepção da padaria. Atrás do balcão existe uma lista: quem
pedir o endereço /cardapio fala com uma função; quem enviar um POST para
/encomendas fala com outra. Quando método e caminho não batem, a recepção não
encontra atendimento para aquele pedido.
No Express, essa lista é a tabela de rotas do app. Cada registro combina
método HTTP, caminho e handler; listen abre a porta de rede por onde os
pedidos chegam. Depois de subir o primeiro servidor, faça duas chamadas ao mesmo
caminho com métodos diferentes e compare os status: essa diferença confirma
que rota não é só URL.
O Express é um pacote do npm como qualquer outro. O projeto começa vazio:
mkdir padaria-api
cd padaria-api
npm init -yRepare na última linha: "type": "commonjs". É o padrão do npm init, e é o
que faz import explodir. Antes de instalar qualquer coisa, troque:
npm pkg set type=module
npm i expressSessenta e oito pacotes para um framework só. Não é desperdício: o Express 5 delega roteamento, leitura de corpo e negociação de tipo para pacotes separados do mesmo time. Se quiser ver qual versão entrou de verdade:
npm ls expressGuarde esse número. Quase todo tutorial antigo na internet fala do Express 4, e
uma diferença de comportamento entre a 4 e a 5 aparece ainda nesta lição — na
seção do express.json(), que é onde ela costuma morder.
Se você pular o npm pkg set type=module, o primeiro node servidor.js
devolve isto:
E se você criar o arquivo antes de instalar o pacote, o erro é outro:
São duas mensagens diferentes para dois problemas diferentes: a primeira é
formato de módulo, a segunda é dependência que falta. Vale conhecer as duas de
cara — a segunda tem uma família inteira de causas, reunidas em
Cannot find module no Node. Se o campo
type ainda soa estranho, a diferença entre os dois formatos está em
ESM ou CommonJS no Node, e o papel de cada campo
do arquivo está em npm e package.json.
app, rota e listen: as três linhas obrigatórias
Crie servidor.js:
import express from 'express';
const app = express();
app.get('/', (req, res) => {
res.send('API da Padaria da Esquina no ar');
});
app.listen(3200, () => {
console.log('Servidor ouvindo em http://localhost:3200');
});node servidor.jsO cursor não volta. Isso é o certo: o processo está vivo, esperando conexão. Em outro terminal, peça a página:
curl -i http://localhost:3200/Três coisas aconteceram, e cada uma tem um nome:
express()cria o app — uma função que sabe receber requisição e devolver resposta, e que carrega dentro dela a lista de rotas.app.get('/', handler)registra uma rota: quando chegar umGETno caminho/, chame esta função. Oreqé o pedido, oresé a resposta.app.listen(3200)abre a porta. Sem essa linha, nada escuta nada.
Não é força de expressão. O mesmo arquivo sem o app.listen roda e termina:
import express from 'express';
const app = express();
app.get('/', (req, res) => {
res.send('API da Padaria da Esquina no ar');
});
console.log('cheguei ao fim do arquivo');O prompt voltou na hora. É o listen que segura o processo de pé — ele deixa um
socket aberto, e o Node só encerra quando não sobra nada para esperar. Quem vem
do servidor HTTP puro do Node reconhece a
cena: por baixo, app.listen é um http.createServer(app).listen(...).
Qualquer caminho que você não registrou responde 404, e o Express escreve a resposta sozinho:
curl -i http://localhost:3200/produtosCannot GET /produtos é a frase que mais aparece na primeira semana de Express.
Ela quase nunca significa “o servidor caiu”: significa que o método e o caminho
que você pediu não batem com nenhuma rota registrada.
Respondendo texto, JSON e HTML na mesma API
Uma API de padaria precisa de duas coisas: uma lista de produtos em JSON, para o site consumir, e um cardápio legível, para colar na tela do balcão.
const produtos = [
{ id: 1, nome: 'Pão francês', preco: 0.9, unidade: 'unidade' },
{ id: 2, nome: 'Sonho de creme', preco: 6.5, unidade: 'unidade' },
{ id: 3, nome: 'Bolo de fubá', preco: 24.9, unidade: 'kg' },
];
app.get('/produtos', (req, res) => {
res.json(produtos);
});
app.get('/cardapio', (req, res) => {
const itens = produtos.map((p) => `<li>${p.nome}</li>`).join('');
res.send(`<h2>Cardápio de hoje</h2><ul>${itens}</ul>`);
});curl -i http://localhost:3200/produtoscurl -i http://localhost:3200/cardapioCompare o Content-Type das duas respostas. Você não escreveu esse cabeçalho em
lugar nenhum: res.json sempre manda application/json e serializa com
JSON.stringify; res.send olha o que recebeu e, para string, assume
text/html. É por isso que a rota / do exemplo anterior, que devolvia texto
simples, também saiu como text/html.
O ETag e o Content-Length também vieram de graça. O ETag é a impressão
digital do corpo. Devolva esse mesmo valor no cabeçalho If-None-Match e a
resposta muda:
curl -s -i -H 'If-None-Match: W/"be-EromempV1Pyrk7AOGUFUIDA8A5I"' \
http://localhost:3200/produtos | head -5304 Not Modified, sem corpo nenhum: o cliente já tem essa versão. Você não
programou isso — veio junto com o res.json.
node –watch: reiniciar sozinho, sem nodemon
Toda vez que você muda o arquivo, precisa parar e subir o servidor de novo. Por
anos a resposta foi instalar o nodemon. Desde o Node 22 isso é uma flag:
node --watch servidor.jsCom o servidor no ar, acrescente uma rota nova ao arquivo e salve:
app.get('/horarios', (req, res) => {
res.json({ abre: '06:00', fecha: '20:00', domingo: '06:00 as 13:00' });
});curl http://localhost:3200/horariosNinguém apertou nada entre salvar e responder. O --watch mata o processo,
sobe outro e imprime o Restarting. Quando o arquivo salvo tem erro de sintaxe,
ele não deixa você no escuro:
Failed running em vez de Completed running: o processo morreu e o watcher
continua vivo, esperando você consertar. Salve de novo com o código correto e
ele sobe sem que você toque no terminal.
A porta fixa no código ou vinda do ambiente
O 3200 cravado no meio do arquivo funciona na sua máquina e falha no deploy,
porque quem hospeda escolhe a porta e informa por variável de ambiente. A
correção é de uma linha:
const PORT = process.env.PORT ?? 3200;
app.listen(PORT, () => {
console.log(`Servidor ouvindo em http://localhost:${PORT}`);
});node servidor.js
# ctrl+C para derrubar, e de novo com a porta vinda do ambiente
PORT=4010 node servidor.jsO número fixo virou padrão de desenvolvimento, e o ambiente manda quando quer.
Repare que usei ?? e não ||: com ||, uma variável valendo 0 ou string
vazia cairia no padrão sem você perceber.
Sobre o número em si: a porta 3000 é a que todo tutorial de Express usa, e é
exatamente por isso que ela vive ocupada. Na máquina em que escrevi esta lição
já havia outro projeto nela, e o Node avisa com EADDRINUSE: address already in use. Escolher 3200 para a padaria custou nada e evitou o conflito. Como o
.env entra nessa história é assunto de
variáveis de ambiente no Node.
express.json(): por que req.body chega undefined sem ele
Publicar cardápio é metade do trabalho. A padaria precisa receber encomenda,
e isso é um POST com corpo:
app.post('/encomendas', (req, res) => {
console.log('req.body =', req.body);
const { cliente, item, quantidade } = req.body;
const encomenda = { id: encomendas.length + 1, cliente, item, quantidade };
encomendas.push(encomenda);
res.status(201).json(encomenda);
});curl -i -X POST http://localhost:3200/encomendas \
-H 'Content-Type: application/json' \
-d '{"cliente":"Dona Marli","item":"Bolo de fubá","quantidade":2}' | head -6E no terminal do servidor:
O corpo chegou pela rede — o curl mandou —, mas o Express não leu. Ele não lê
corpo por padrão: a requisição chega como um fluxo de bytes e alguém precisa
juntar os pedaços e decidir que aquilo é JSON. Esse alguém é o express.json(),
registrado uma vez, antes das rotas:
const app = express();
app.use(express.json());curl -i -X POST http://localhost:3200/encomendas \
-H 'Content-Type: application/json' \
-d '{"cliente":"Dona Marli","item":"Bolo de fubá","quantidade":2}'Agora a parte que pega mesmo quem já sabe disso. Com o express.json() no
lugar, mande o mesmo POST sem o cabeçalho Content-Type:
curl -i -X POST http://localhost:3200/encomendas \
-d '{"cliente":"Seu Jorge","item":"Pão francês","quantidade":10}'Mesma mensagem, mas olhe a última linha da pilha: agora ela passa por
body-parser/lib/read.js. O express.json() rodou, olhou o Content-Type que
o curl mandou por padrão e decidiu que aquilo não era JSON — então não tocou
no req.body. No Express 4 ele ficava {} nesse caso; no 5 ele fica
undefined. Se um tutorial antigo prometeu objeto vazio, é essa a mudança.
A estrutura de pastas que vale desde o primeiro dia
Com quatro rotas o arquivo único já incomoda. A divisão mínima que resolve por muito tempo separa o que a API é do como ela sobe:
padaria-api/
├── .gitignore
├── package.json
├── package-lock.json
└── src
├── app.js
└── servidor.jssrc/app.js monta o app, registra middlewares e rotas e exporta — sem abrir
porta nenhuma:
import express from 'express';
export const app = express();
app.use(express.json());
const encomendas = [];
const produtos = [
{ id: 1, nome: 'Pão francês', preco: 0.9, unidade: 'unidade' },
{ id: 2, nome: 'Sonho de creme', preco: 6.5, unidade: 'unidade' },
{ id: 3, nome: 'Bolo de fubá', preco: 24.9, unidade: 'kg' },
];
app.get('/', (req, res) => {
res.send('API da Padaria da Esquina no ar');
});
app.get('/produtos', (req, res) => {
res.json(produtos);
});
app.get('/encomendas', (req, res) => {
res.json(encomendas);
});
app.post('/encomendas', (req, res) => {
const { cliente, item, quantidade } = req.body;
const encomenda = { id: encomendas.length + 1, cliente, item, quantidade };
encomendas.push(encomenda);
res.status(201).json(encomenda);
});src/servidor.js só liga a chave:
import { app } from './app.js';
const PORT = process.env.PORT ?? 3200;
app.listen(PORT, () => {
console.log(`Servidor ouvindo em http://localhost:${PORT}`);
});Essa separação não é frescura de arquitetura: teste automatizado importa o app
e faz requisição sem abrir porta, e é isso que evita que a sua suíte trave
tentando escutar duas vezes na mesma porta. Feche registrando os dois comandos
no package.json:
{
"type": "module",
"scripts": {
"start": "node src/servidor.js",
"dev": "node --watch src/servidor.js"
},
"dependencies": {
"express": "^5.2.1"
}
}npm run devUma última coisa sobre esse array encomendas: ele vive na memória do processo.
Mande um POST, confira, salve o arquivo para o --watch reiniciar e confira
de novo:
curl -X POST http://localhost:3200/encomendas \
-H 'Content-Type: application/json' \
-d '{"cliente":"Dona Marli","item":"Bolo de fubá","quantidade":2}'
curl http://localhost:3200/encomendas
# salva o arquivo, o --watch reinicia
curl http://localhost:3200/encomendasA encomenda da Dona Marli evaporou no reinício. Não é bug: é o lembrete de que array em memória é rascunho, e que banco de dados entra na história assim que a API precisa lembrar de alguma coisa entre dois deploys.
Confirmando pelo curl antes de abrir o navegador
O navegador é péssimo cliente de API. Ele só faz GET na barra de endereço,
esconde os cabeçalhos, formata o JSON do jeito dele e ainda pede um
/favicon.ico que suja o seu log. O curl mostra o que realmente trafegou.
| o que você quer saber | comando | o que aparece |
|---|---|---|
| se subiu | curl http://localhost:3200/ |
só o corpo da resposta |
| qual status e qual tipo | curl -i http://localhost:3200/produtos |
os cabeçalhos antes do corpo |
| um resumo de várias rotas | curl -s -o /dev/null -w "%{http_code} %{content_type}\n" ... |
uma linha por rota |
| se o POST funciona | curl -X POST -H 'Content-Type: application/json' -d '{...}' |
a resposta do verbo certo |
Aquela terceira linha é a que eu mais uso para conferir uma API inteira de enfiada:
for r in / /produtos /encomendas /cardapio; do
curl -s -o /dev/null -w "%{http_code} %{content_type} $r\n" "http://localhost:3200$r"
doneA última linha é a prova de que isso serve para alguma coisa: a rota /cardapio
ficou para trás quando movi o código para src/app.js, e eu só descobri aqui.
No navegador ela apareceria como uma página de erro qualquer, fácil de confundir
com “o servidor caiu”; nesta lista, um 404 no meio de três 200 diz
exatamente o que houve.
E o teste que mais economiza tempo é mandar o verbo errado de propósito:
curl -i -X POST http://localhost:3200/produtos | head -3POST em rota registrada como GET dá 404, não 405. Método e caminho formam a
chave da rota no Express: mudar um dos dois é procurar outra rota. Guarde isso,
porque é a causa de metade dos Cannot POST que aparecem quando o formulário do
front começa a conversar com a API.
O que vem depois
A API responde, recebe corpo e reinicia sozinha. Falta ela distinguir quem
está pedindo o quê — o id na URL, o filtro na query string, o corpo no POST —,
e é isso que vem em
req.params, req.query e req.body no Express.
Depois disso, express.Router divide as rotas por recurso e o tratamento de
erro deixa de ser um try/catch por rota. O caminho inteiro, na ordem, está na
trilha de Node.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Preciso instalar o nodemon para o servidor reiniciar sozinho?
Qual a diferença entre res.send e res.json?
Dá para usar require em vez de import com o Express?
Por que o npm instala 68 pacotes para um Express só?
Posso deixar as rotas todas em um arquivo só?
Todo o código deste artigo foi executado em Node 24.16.0 com Express 5.2.1, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- Express — Hello world example — expressjs.com
- Express 5 — express.json() — expressjs.com
- Node.js CLI — a flag --watch — nodejs.org


