O que é uma API REST: recurso, verbo, status e contrato
Recurso, verbo HTTP, status code e stateless explicados desenhando o contrato de uma API de tarefas, com cada requisição feita de verdade no curl.
Uma API REST é um combinado: cada coisa que o sistema guarda vira um endereço
(/tarefas, /tarefas/2), e o que você quer fazer com ela vira um verbo do
HTTP (GET, POST, PUT, DELETE). O servidor responde com um número de
status e uma representação em JSON. É esse combinado que deixa um app feito por
uma pessoa conversar com um back-end feito por outra.
Toda esta lição é a API da Escola Vila Nova: professores publicam tarefas
para as turmas, e o app dos alunos consome essas tarefas. O servidor roda em
Node 24.16.0 com Express 5.2.1, num MacBook, e cada resposta colada aqui saiu do
curl do meu terminal — inclusive os erros.
Um balcão com endereços, pedidos e recibos
Imagine a secretaria da Escola Vila Nova. Cada arquivo tem um endereço claro: as tarefas ficam num armário, e a tarefa 2 fica numa pasta específica. A pessoa chega ao balcão, diz se quer consultar, criar, trocar ou apagar algo e recebe um recibo dizendo o que aconteceu. Ela não entra no arquivo nem precisa saber como a escola o organiza por dentro.
Na API REST, o armário é o recurso, o endereço é a URL, o pedido usa um
método HTTP, e o recibo é o status code acompanhado de uma representação,
normalmente JSON. Ao ler as primeiras chamadas com curl, tente nomear essas
quatro peças antes de olhar a explicação. Se endereço, verbo e resposta contam a
mesma história, o contrato está funcionando.
Elas aparecem juntas o tempo todo e significam coisas diferentes.
API é interface de programação: o combinado de como um programa fala com
outro. Math.random() é API. O botão de compartilhar do celular é API. Nem toda
API passa pela rede.
HTTP é o protocolo — o formato do envelope que vai e volta pela rede. Peça
ao curl o envelope inteiro com -v: as linhas abertas por > são o que saiu
da sua máquina, as abertas por < são o que voltou do servidor, e as com
asterisco são o curl narrando a conexão.
curl -v http://localhost:3939/tarefas/2Uma requisição HTTP tem três partes fixas — método (GET), caminho
(/tarefas/2) e cabeçalhos — e, às vezes, um corpo. A resposta segue o
mesmo desenho: status (200 OK), cabeçalhos, uma linha em branco e o
corpo.
REST não é nada disso. É um estilo de usar o HTTP, descrito por Roy Fielding em 2000. Não se instala, não se importa, não tem versão. É um conjunto de decisões sobre como nomear as coisas e qual verbo usar em cada uma.
O arquivo que implementa esse ciclo começa assim. As rotas entram no meio, e cada seção desta lição acrescenta uma:
import express from 'express';
const app = express();
app.use(express.json());
let proximoId = 4;
const tarefas = [
{ id: 1, titulo: 'Ler o capítulo 3 de Dom Casmurro', turma: '9A', professor: 'marina', prazo: '2026-06-15', entregue: false },
{ id: 2, titulo: 'Lista de equações do 2º grau', turma: '9A', professor: 'heitor', prazo: '2026-06-12', entregue: true },
{ id: 3, titulo: 'Maquete do sistema solar', turma: '7B', professor: 'marina', prazo: '2026-06-20', entregue: false },
];
app.listen(3939, () => {
console.log('API da Escola Vila Nova em http://localhost:3939/tarefas');
});Para rodar isso na sua máquina são três comandos numa pasta vazia. O
type: module é o que libera o import dentro de um arquivo .js:
npm init -y
npm pkg set type=module
npm install expressCom o código salvo em servidor.js, a API sobe e fica esperando:
node servidor.jsEsse terminal fica ocupado com o servidor; os curl daqui para baixo saem de um
segundo terminal.
O Express é só o carregador de rotas aqui; o que ele faz linha a linha é assunto da primeira rota no Express. O que interessa nesta lição é o combinado, não o framework.
Recurso é substantivo: /tarefas, nunca /buscarTarefa
Recurso é a coisa que a sua API expõe. Na Vila Nova são quatro: tarefa, turma, aluno e professor. Cada uma vira um caminho, no plural e em minúsculas.
| o que é | caminho | devolve |
|---|---|---|
| coleção | /tarefas |
todas as tarefas |
| item | /tarefas/2 |
a tarefa de id 2 |
| subcoleção | /turmas/9A/tarefas |
as tarefas da turma 9A |
A coleção é a porta de entrada, e o handler dela não faz nada além de devolver o array inteiro:
app.get('/tarefas', (req, res) => {
res.json(tarefas);
});O curl mostra o JSON como ele viaja — uma linha só, sem espaço nenhum — então
joguei a saída no json_pp, que já vem no macOS e na maioria das distribuições
Linux:
curl -s http://localhost:3939/tarefas | json_ppRepare no que não aparece nos caminhos: nenhum verbo. Nada de
/buscarTarefa, /criarTarefa, /deletarTarefa. O caminho diz qual coisa;
quem diz o que fazer é o método HTTP. É por isso que a API inteira cabe em
três caminhos — /tarefas, /tarefas/:id e /turmas/:turma/tarefas — em vez
de vinte.
Vale ver o estrago do jeito errado, que existe em muito código real:
// como NÃO fazer: verbo no caminho e status 200 para tudo
app.get('/buscarTarefa', (req, res) => {
const tarefa = tarefas.find((t) => t.id === Number(req.query.id));
res.json({ sucesso: Boolean(tarefa), tarefa: tarefa ?? null });
});curl -i "http://localhost:3939/buscarTarefa?id=99"{“sucesso”:false,“tarefa”:null}
A tarefa 99 não existe e o servidor respondeu 200 OK. Para o navegador, para
o cache, para o monitoramento e para qualquer biblioteca de HTTP, essa
requisição deu certo. O erro ficou escondido dentro do corpo, onde só o seu if
enxerga.
O mesmo pedido no estilo REST tem este handler:
app.get('/tarefas/:id', (req, res) => {
const tarefa = tarefas.find((t) => t.id === Number(req.params.id));
if (!tarefa) {
return res.status(404).json({ erro: `Tarefa ${req.params.id} não existe` });
}
res.json(tarefa);
});curl -i http://localhost:3939/tarefas/99{“erro”:“Tarefa 99 não existe”}
Mesmo dado faltando, resposta honesta. O :id do caminho chega no handler
dentro de req.params — a diferença entre ele, req.query e req.body tem
uma lição só para ela mais para a frente.
O verbo carrega a intenção — e é aí que entra a idempotência
Com o recurso definido, o método HTTP escolhe a operação. São cinco no dia a dia:
| verbo | o que faz | leva corpo | idempotente |
|---|---|---|---|
GET |
lê, e nunca muda nada | não | sim |
POST |
cria um item novo na coleção | sim | não |
PUT |
substitui o item inteiro | sim | sim |
PATCH |
altera alguns campos do item | sim | depende |
DELETE |
remove o item | não | sim |
Idempotente é a palavra que cai em entrevista e assusta sem motivo. Quer dizer: repetir a mesma requisição deixa o servidor no mesmo estado em que uma única chamada deixaria. Não é a resposta que precisa ser igual — é o efeito.
Dá para provar. O handler do POST é este:
app.post('/tarefas', (req, res) => {
const nova = {
id: proximoId++,
titulo: req.body.titulo,
turma: req.body.turma,
professor: req.body.professor,
prazo: req.body.prazo,
entregue: false,
};
tarefas.push(nova);
res.status(201).location(`/tarefas/${nova.id}`).json(nova);
});Mandei exatamente esta requisição, duas vezes seguidas:
curl -i -X POST http://localhost:3939/tarefas \
-H "Content-Type: application/json" \
-d '{"titulo":"Resumo do capítulo 4","turma":"9A","professor":"marina","prazo":"2026-06-22"}'{“id”:4,“titulo”:“Resumo do capítulo 4”,“turma”:“9A”,“professor”:“marina”,“prazo”:“2026-06-22”,“entregue”:false}
E a segunda, mandada logo em seguida:
{“id”:5,“titulo”:“Resumo do capítulo 4”,“turma”:“9A”,“professor”:“marina”,“prazo”:“2026-06-22”,“entregue”:false}
Duas tarefas, ids 4 e 5. É por isso que POST não é idempotente, e é por
isso que o botão “salvar” clicado duas vezes num sinal ruim de celular cria dois
cadastros.
Agora o PUT, que manda o item inteiro e substitui o que estava lá:
app.put('/tarefas/:id', (req, res) => {
const i = tarefas.findIndex((t) => t.id === Number(req.params.id));
if (i === -1) return res.status(404).json({ erro: 'Tarefa não existe' });
tarefas[i] = {
id: tarefas[i].id,
titulo: req.body.titulo,
turma: req.body.turma,
professor: req.body.professor,
prazo: req.body.prazo,
entregue: req.body.entregue ?? false,
};
res.json(tarefas[i]);
});curl -i -X PUT http://localhost:3939/tarefas/1 \
-H "Content-Type: application/json" \
-d '{"titulo":"Ler o capítulo 3 de Dom Casmurro","turma":"9A","professor":"marina","prazo":"2026-06-18","entregue":true}'{“id”:1,“titulo”:“Ler o capítulo 3 de Dom Casmurro”,“turma”:“9A”,“professor”:“marina”,“prazo”:“2026-06-18”,“entregue”:true}
Repetindo o mesmo comando, a resposta volta idêntica — inclusive o ETag, que é
uma assinatura do conteúdo:
{“id”:1,“titulo”:“Ler o capítulo 3 de Dom Casmurro”,“turma”:“9A”,“professor”:“marina”,“prazo”:“2026-06-18”,“entregue”:true}
Duas chamadas, um resultado só, nenhuma tarefa a mais. Esse é o sentido prático de idempotência: dá para repetir sem medo, e é o que permite ao app tentar de novo quando a rede cai no meio do envio.
O PATCH existe para o caso mais comum da escola: o aluno marcou a tarefa como
entregue e nada mais mudou.
app.patch('/tarefas/:id', (req, res) => {
const tarefa = tarefas.find((t) => t.id === Number(req.params.id));
if (!tarefa) return res.status(404).json({ erro: 'Tarefa não existe' });
Object.assign(tarefa, req.body, { id: tarefa.id });
res.json(tarefa);
});curl -i -X PATCH http://localhost:3939/tarefas/3 \
-H "Content-Type: application/json" -d '{"entregue":true}'{“id”:3,“titulo”:“Maquete do sistema solar”,“turma”:“7B”,“professor”:“marina”,“prazo”:“2026-06-20”,“entregue”:true}
Um campo no corpo, o resto intacto. Se essa mesma tela usasse PUT mandando só
{"entregue":true}, o título, a turma e o prazo virariam undefined — o PUT
substitui, não mescla. É o erro de design mais caro desta lição.
O DELETE mostra a parte que confunde. O handler tira o item do array e
responde sem corpo nenhum:
app.delete('/tarefas/:id', (req, res) => {
const i = tarefas.findIndex((t) => t.id === Number(req.params.id));
if (i === -1) return res.status(404).json({ erro: 'Tarefa não existe' });
tarefas.splice(i, 1);
res.status(204).end();
});Apaguei a tarefa 5 e mandei o mesmo comando outra vez:
curl -i -X DELETE http://localhost:3939/tarefas/5Depois dos cabeçalhos não veio nada: 204 quer dizer “deu certo e não tenho o
que te mostrar”. Na segunda vez, com a tarefa já apagada:
{“erro”:“Tarefa não existe”}
O status mudou de 204 para 404, mas o estado do servidor é o mesmo nos dois
casos: a tarefa 5 não existe. DELETE continua idempotente. O que muda é só o
relato.
E o servidor sabe dizer quais verbos aceita em cada caminho. O Express responde
OPTIONS sozinho, montando o cabeçalho Allow a partir das rotas que você
registrou:
curl -i -X OPTIONS http://localhost:3939/tarefasGET, HEAD, POST
curl -i -X OPTIONS http://localhost:3939/tarefas/2DELETE, GET, HEAD, PATCH, PUT
Esses dois Allow são o desenho da API inteira em duas linhas: na coleção
você lista e cria; no item você lê, substitui, altera e apaga. Não existe
DELETE /tarefas, porque apagar todas as tarefas da escola de uma vez não é uma
operação que alguém deva conseguir fazer sem querer.
O status code é a primeira coisa que o cliente lê
O número de três dígitos vem antes de qualquer JSON, e o primeiro dígito já conta a história: 2xx deu certo, 3xx mudou de lugar, 4xx o cliente errou, 5xx o servidor errou. Essa divisão é o que decide quem é acordado às duas da manhã.
| status | quando a Vila Nova usa | corpo |
|---|---|---|
200 OK |
leitura, PUT e PATCH que deram certo |
o recurso |
201 Created |
POST que criou a tarefa |
o recurso criado, mais o cabeçalho Location |
204 No Content |
DELETE que apagou |
vazio, de propósito |
400 Bad Request |
faltou titulo no corpo |
o que estava errado |
401 Unauthorized |
não veio quem é o professor | como se identificar |
404 Not Found |
a tarefa 99 não existe | uma mensagem curta |
415 Unsupported Media Type |
o corpo não veio como JSON | o formato aceito |
500 Internal Server Error |
o seu código quebrou | nada de detalhe interno |
Dois detalhes separam uma API amadora de uma profissional, e os dois estão no
201 de agora há pouco. O primeiro é o cabeçalho Location: /tarefas/4: o
servidor diz onde a coisa nova mora, e o cliente não precisa adivinhar o
endereço. O segundo é devolver o recurso criado já com o id preenchido — sem
isso, o app teria que fazer outra requisição só para descobrir o número.
Do outro lado da tabela, o 204 é o único que não pode ter corpo — mandar
JSON junto com ele é erro de protocolo, não questão de estilo. Fora da tabela
existe outro caso assim, o 304 Not Modified, e ele aparece no fim desta lição.
A tabela completa, com os status que aparecem menos, está em
métodos HTTP e status code.
Representação: o JSON é uma foto do recurso, não o recurso
O recurso é a tarefa — uma linha no banco, um objeto na memória do servidor. O
que viaja pela rede nunca é a tarefa: é uma representação dela. O cabeçalho
Content-Type avisa em que formato a foto foi tirada, e o Accept, na
requisição, é o cliente dizendo em que formato prefere receber.
Quando o cliente manda um corpo, é ele quem precisa declarar o formato. E aqui
mora o erro que derruba todo mundo na primeira API. Mandei o POST esquecendo o
cabeçalho, e cortei a saída nos cabeçalhos porque o corpo é uma página HTML de
erro:
curl -i -X POST http://localhost:3939/tarefas \
-d '{"titulo":"Ficha de leitura","turma":"7B","professor":"marina","prazo":"2026-06-25"}' | head -9No terminal onde o servidor está rodando, apareceu o motivo:
A linha 37 do servidor é titulo: req.body.titulo, e o erro diz que req.body
está undefined. Sem o Content-Type, o curl mandou o corpo como
application/x-www-form-urlencoded; o express.json() olhou o cabeçalho, viu
que não era JSON e não fez nada — sem reclamar. O JSON chegou ao servidor e
ninguém o traduziu.
A correção é o servidor parar de confiar no cliente. Duas verificações no começo do handler resolvem, e cada uma tem o seu status:
app.post('/tarefas', (req, res) => {
if (!req.is('application/json')) {
return res.status(415).json({ erro: 'Envie o cabeçalho Content-Type: application/json' });
}
if (!req.body.titulo || !req.body.turma) {
return res.status(400).json({ erro: 'titulo e turma são obrigatórios' });
}
// ... daqui para baixo, o mesmo código de antes
});Com isso no lugar, a requisição sem cabeçalho vira uma resposta que ensina o cliente a se corrigir:
curl -i -X POST http://localhost:3939/tarefas \
-d '{"titulo":"Ficha de leitura","turma":"7B","professor":"marina","prazo":"2026-06-25"}'{“erro”:“Envie o cabeçalho Content-Type: application/json”}
E um JSON legítimo, mas sem o título, também:
curl -i -X POST http://localhost:3939/tarefas \
-H "Content-Type: application/json" -d '{"turma":"7B","professor":"marina"}'{“erro”:“titulo e turma são obrigatórios”}
Duas linhas de if transformaram um 500 (culpa minha, o servidor quebrou) em
415 e 400 (culpa do cliente, e ele foi avisado do que fazer). Escrever isso
campo a campo cansa rápido, e é para isso que existem bibliotecas de validação —
assunto de validar a entrada da API. Se o JSON
ainda é meio estranho para você, a lição de
JSON em JavaScript mostra o parse e o
stringify que o Express chama por baixo.
Stateless: o servidor esquece você entre uma chamada e outra
Essa é a restrição do REST que mais gente ignora e a que mais dói depois. Stateless quer dizer que cada requisição carrega tudo que o servidor precisa para respondê-la. Não existe “de onde paramos”.
A rota abaixo só responde a quem se identifica no cabeçalho:
app.get('/minhas-tarefas', (req, res) => {
const professor = req.header('X-Professor');
if (!professor) {
return res.status(401).json({ erro: 'Envie o cabeçalho X-Professor em toda requisição' });
}
res.json(tarefas.filter((t) => t.professor === professor));
});Chamei duas vezes, uma atrás da outra, tirando o cabeçalho na segunda:
curl -i http://localhost:3939/minhas-tarefas -H "X-Professor: marina"[{“id”:1,“titulo”:“Ler o capítulo 3 de Dom Casmurro”,“turma”:“9A”,“professor”:“marina”,“prazo”:“2026-06-18”,“entregue”:true},{“id”:3,“titulo”:“Maquete do sistema solar”,“turma”:“7B”,“professor”:“marina”,“prazo”:“2026-06-20”,“entregue”:true},{“id”:4,“titulo”:“Resumo do capítulo 4”,“turma”:“9A”,“professor”:“marina”,“prazo”:“2026-06-22”,“entregue”:false}]
curl -i http://localhost:3939/minhas-tarefas{“erro”:“Envie o cabeçalho X-Professor em toda requisição”}
Um instante antes, o servidor sabia que era a professora Marina. Na chamada seguinte, não sabia mais nada. Um middleware de três linhas, colado antes das rotas, mostra exatamente o que ele viu nas duas:
app.use((req, res, next) => {
console.log(`${req.method} ${req.originalUrl} · X-Professor: ${req.header('X-Professor') ?? '(ausente)'}`);
next();
});Parece um defeito e é uma escolha de projeto. Se o servidor não guarda nada
sobre você, qualquer cópia dele consegue atender a sua próxima requisição — dá
para pôr três máquinas atrás de um balanceador de carga e cair em qualquer uma
delas sem perder o fio. Foi assim que a web escalou. O preço é que o cliente
repete a identificação em toda chamada, e no mundo real isso vira o token do JWT
viajando no cabeçalho Authorization de cada requisição.
O contrato da Vila Nova, escrito antes do código
Falta uma rota para o desenho ficar completo: a subcoleção, que é o que o app abre quando o aluno escolhe a turma dele. Repare que ela filtra por turma, e não inventa um caminho novo para isso.
app.get('/turmas/:turma/tarefas', (req, res) => {
const daTurma = tarefas.filter((t) => t.turma === req.params.turma.toUpperCase());
res.json(daTurma);
});curl -i http://localhost:3939/turmas/9a/tarefas[{“id”:1,“titulo”:“Ler o capítulo 3 de Dom Casmurro”,“turma”:“9A”,“professor”:“marina”,“prazo”:“2026-06-18”,“entregue”:true},{“id”:2,“titulo”:“Lista de equações do 2º grau”,“turma”:“9A”,“professor”:“heitor”,“prazo”:“2026-06-12”,“entregue”:true},{“id”:4,“titulo”:“Resumo do capítulo 4”,“turma”:“9A”,“professor”:“marina”,“prazo”:“2026-06-22”,“entregue”:false}]
Três tarefas da 9A, com a tarefa 1 já atualizada pelo PUT e a tarefa 4 criada
pelo POST lá em cima. Uma coleção que não devolve nada não é erro: uma turma
sem tarefa responde 200 com uma lista vazia, e não 404. O 404 é para o
recurso que não existe, não para a lista que está vazia.
Junte recurso, verbo e status e você tem o contrato — o documento que o front-end precisa para começar a trabalhar sem esperar o seu back-end ficar pronto. É a primeira coisa que eu escrevo, sempre em tabela, sempre antes da primeira linha de código:
| verbo e caminho | o que faz | corpo que entra | resposta |
|---|---|---|---|
GET /tarefas |
lista tudo | — | 200 + array |
GET /tarefas/:id |
uma tarefa | — | 200 + objeto, ou 404 |
POST /tarefas |
cria | titulo, turma, professor, prazo |
201 + Location, ou 400 |
PUT /tarefas/:id |
substitui inteira | o objeto completo | 200, ou 404 |
PATCH /tarefas/:id |
muda um campo | só o que mudou | 200, ou 404 |
DELETE /tarefas/:id |
apaga | — | 204, ou 404 |
GET /turmas/:turma/tarefas |
as tarefas da turma | — | 200 + array |
Sete linhas descrevem a API inteira, e nenhuma delas fala de Express, de banco de dados ou de linguagem. Esse mesmo contrato pode ser reescrito em Python ou em Go sem o app do aluno perceber a troca — é essa a promessa do REST.
Quando o projeto cresce, a tabela vira um arquivo no formato OpenAPI, que ferramentas leem para gerar documentação e até o cliente HTTP pronto. Mas a tabela vem primeiro; o formato é detalhe.
Onde REST não é a melhor escolha
REST é ótimo para operações sobre recursos bem definidos, que é a maioria esmagadora do trabalho. Ele começa a incomodar em dois pontos.
O primeiro é a quantidade de idas e voltas. A tela inicial do app da Vila Nova mostra as turmas e as tarefas de cada uma; com recursos separados, isso são três requisições em vez de uma. Falta um recurso para a conta fechar — a lista de turmas:
app.get('/turmas', (req, res) => {
const nomes = [...new Set(tarefas.map((t) => t.turma))];
res.json(nomes.map((nome) => ({ nome })));
});Medi as duas situações contra o localhost, com curl -w "%{time_total}", em
cinco rodadas:
for i in 1 2 3 4 5; do
um=$(curl -s -o /dev/null -w "%{time_total}" http://localhost:3939/tarefas)
a=$(curl -s -o /dev/null -w "%{time_total}" http://localhost:3939/turmas)
b=$(curl -s -o /dev/null -w "%{time_total}" http://localhost:3939/turmas/9A/tarefas)
c=$(curl -s -o /dev/null -w "%{time_total}" http://localhost:3939/turmas/7B/tarefas)
node -e "const [u,a,b,c]=process.argv.slice(1).map(Number);console.log('rodada $i 1 requisicao: '+(u*1000).toFixed(1)+'ms 3 requisicoes: '+((a+b+c)*1000).toFixed(1)+'ms')" $um $a $b $c
doneNa minha máquina a diferença é irrelevante, porque a rede entre o curl e o
servidor é zero. O número que importa aí não é o tempo: é o três. Troque o
localhost por um servidor de verdade e cada ida e volta passa a custar dezenas
de milissegundos de latência, que se multiplicam pela quantidade de chamadas. É
esse o problema que o GraphQL nasceu para resolver, deixando o cliente pedir num
único envelope exatamente os campos que vai usar.
O segundo ponto é o tempo real. Em REST é sempre o cliente que pergunta; se a professora publica uma tarefa agora, o app do aluno só descobre na próxima pergunta. Para o servidor avisar sozinho, o caminho é WebSocket ou Server-Sent Events. E entre serviços de back-end, onde ninguém precisa ler o JSON com olho humano, o gRPC troca legibilidade por velocidade.
Nenhum desses substitui REST — eles convivem no mesmo sistema. A API pública
continua REST porque qualquer pessoa consegue testá-la com um curl, como você
acabou de fazer.
REST, RESTful e as regras que quase ninguém segue
A definição original tem seis restrições: cliente-servidor, stateless, cache, interface uniforme, sistema em camadas e código sob demanda. As duas primeiras você viu funcionando página acima.
A terceira, cache, esteve o tempo todo naquele ETag que apareceu em quase
toda resposta. Se o cliente devolve a assinatura que recebeu, no cabeçalho
If-None-Match, o servidor compara e responde que nada mudou:
curl -i http://localhost:3939/tarefas/2 \
-H 'If-None-Match: W/"7a-Q+ogjIvtZpe8rnV6gn4vnbrl8os"'304 Not Modified, sem uma linha de JSON no fio — é o segundo status que não
pode ter corpo. A cópia que o cliente já tinha continua valendo, e quem ganhou o
tráfego economizado foi todo mundo no caminho. Repare que essa é a mesma
assinatura da primeira resposta desta lição: a tarefa 2 nunca foi alterada.
A quarta, interface uniforme, é o pacote que esta lição inteira montou: recurso no caminho, verbo no método, status na resposta, representação no corpo.
Falta a parte que quase ninguém implementa: cada resposta deveria trazer os links
do que se pode fazer em seguida — o tal do HATEOAS. Na prática, o mais perto
disso que uma API brasileira típica chega é o cabeçalho Location do 201 que
apareceu ali em cima.
Uma decisão pragmática para a primeira API: não perca o sono com HATEOAS. Comece por recurso bem nomeado, método adequado, status coerente, JSON previsível e mensagem de erro que ajuda. Fazer bem essas cinco coisas cria um contrato que cliente, teste e documentação conseguem entender; HATEOAS pode entrar quando o produto realmente precisar orientar ações por links.
O que vem depois
O contrato está de pé; falta escrever o servidor com calma. A próxima lição
instala o Express e monta a
primeira rota do zero, explicando cada linha
do arquivo que apareceu picotado aqui — e mostrando por que ninguém escreve API
no node:http cru depois de conhecer o
framework. O caminho inteiro, daqui até o deploy, está no
guia de Node.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
API REST e RESTful são a mesma coisa?
Preciso de banco de dados para criar uma API REST?
Dá para uma API REST devolver XML ou CSV em vez de JSON?
Por que a resposta precisa de status code se o corpo já diz o que houve?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
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
- MDN — HTTP request methods — developer.mozilla.org
- MDN — HTTP response status codes — developer.mozilla.org
- RFC 9110 — HTTP Semantics — rfc-editor.org
- Roy Fielding — Architectural Styles and the Design of Network-based Software Architectures, cap. 5 — ics.uci.edu



