A motivação desse projeto foi pra entender como funciona certos conceitos de um Express da vida. Fazer uma API REST com Express é bem simples, já que ele te entrega uma estrutura pronta.
Pra fazer essa API eu precisava de uma ideia de projeto legal, pra não ser só mais um TodoList API. E é tentador fazer esse tipo de projeto dada a complexidade, pra ser sincero.
Então lembrei daquele Bit.ly (encurtador de links), e o veredito foi esse mesmo.
Antes de começar
Não iremos precisar de um package.json porque não teremos dependências — somente módulos nativos do Node. E por conta disso, não teremos TypeScript também.
Nota: Esse projeto não deve ser colocado em produção. Isso é um "projeto de laboratório" e deve ser considerado como tal.
Criando um servidor Node.js
Pra lidar com o protocolo HTTP podemos utilizar o módulo nativo node:http, pois ele possui uma função chamada createServer que iremos usar pra criar o servidor.
Essa função recebe um parâmetro chamado requestListener — uma callback que fica ouvindo requisições:
createServer(() => {});
Essa callback possui dois parâmetros, request e response, e é com eles que podemos manipular o que está vindo do mundo externo: corpo de requisição, respostas, headers...
createServer((request, response) => {});
Essa função retorna alguns métodos, mas o mais importante pra gente agora é o .listen, que faz nosso servidor ficar aberto em uma porta de rede específica como 3000, 8080, 3001.
No primeiro parâmetro temos a porta e no segundo uma callback que normalmente é usada para criar logs informando que o servidor está rodando corretamente:
.listen(3000, () => {
console.log("Server started at http://localhost:3000")
});
Dito isso, podemos criar um servidor assim:
import { createServer } from "node:http";
const app = createServer((request, response) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ ok: true }));
});
app.listen(3000, () => {
console.log("Server started at http://localhost:3000");
});
- O método
.writeHeadserve pra escrever os cabeçalhos HTTP e o status code da requisição. - O método
.endserve pra finalizar a requisição enviando como resposta uma string no formato JSON.
Pra iniciar o servidor basta abrir um terminal e rodar:
node index.js
Porém se você acessar qualquer endpoint, o Node irá sempre retornar {"ok":true} como resposta.
Como criar endpoints no Node.js?
Essa distinção de rotas por métodos e endpoints vem do padrão REST.
O Node não tem internamente esse mecanismo de manipulação de rotas. Isso significa que se quisermos ter rotas que façam coisas diferentes de acordo com o método HTTP e endpoint, teremos que implementar do zero!
Vamos modificar um pouco nosso index.js:
const links = [
{
id: "9d66dd18-0125-4677-8fd1-3a541da16e1d",
original_url: "https://wesleydmscn.com/",
short_code: "wesleydmscn",
created_at: "2025-04-05T13:37:01.869Z",
},
];
const app = createServer((request, response) => {
if (request.url === "/links" && request.method === "GET") {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify(links));
} else {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ ok: true }));
}
});
app.listen(3000, () => {
console.log("Server started at http://localhost:3000");
});
Acima criamos uma lista em memória pra guardar os links e adicionamos um endpoint usando condicionais em cima das propriedades url e method do objeto request.
Agora se acessarmos http://localhost:3000/links o servidor irá responder com:
[
{
"id": "9d66dd18-0125-4677-8fd1-3a541da16e1d",
"original_url": "https://wesleydmscn.com/",
"short_code": "wesleydmscn",
"created_at": "2025-04-05T13:37:01.869Z"
}
]
Legal né? Mas não dá pra ficar criando endpoint desse jeito — muitos ifs, legibilidade vai pro ralo...
Então vamos refatorar esse código pra simplificar a criação de novos endpoints.
Organizando nosso código
- Vamos mover nosso
index.jspra uma pastasrc/ - Vamos mover nossa lista
linksprasrc/mocks/links.js - Criar um arquivo
src/route.jspra centralizar as rotas
// src/mocks/links.js
export default [
{
id: "9d66dd18-0125-4677-8fd1-3a541da16e1d",
original_url: "https://wesleydmscn.com/",
short_code: "wesleydmscn",
created_at: "2025-04-05T13:37:01.869Z",
},
];
// src/route.js
export default [
{
endpoint: "/links",
method: "GET",
handler: (request, response) => {},
},
];
Nota: O handler é a função que vai fornecer o recurso — ela será responsável por devolver os dados.
Como nossa "entidade" é um link, vamos criar um controller pra centralizar todas as funções handler e importar nossa lista links pra usar como banco de dados em memória:
// src/controllers/link.controller.js
import rawLinks from "../mocks/links.js";
let links = rawLinks;
export default {
listLinks(request, response) {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify(links));
},
}
Utilizar nosso controller no endpoint de buscar todos os links:
// src/route.js
import linkController from "./controllers/link.controller.js";
export default [
{
endpoint: "/links",
method: "GET",
handler: linkController.listLinks,
},
]
Com isso sua estrutura de pastas deve ficar assim:
└── src/
├── controllers/
│ └── link.controller.js
├── mocks/
│ └── links.js
├── index.js
└── route.js
Refatorando o src/index.js
O código já tá bem legal, mas falta fazer funcionar haha. Vamos mudar a lógica de condicionais para uma mais simples:
// src/index.js
import routes from "./route.js";
const app = createServer((request, response) => {
const route = routes.find(
({ endpoint, method }) => endpoint === request.url && method === request.method
);
if (route) {
return route.handler(request, response);
}
response.writeHead(404, { "content-type": "application/json" });
response.end(
JSON.stringify({ error: `Cannot ${request.method} ${request.url}` })
);
});
Agora nosso servidor só irá responder se existir a rota. Caso não exista, ele irá responder com 404 e uma mensagem de erro em JSON: "Cannot GET /outro-endpoint".
É muito comum uma API ter um logger. Vamos adicionar um simples pra toda vez que alguém mandar uma requisição termos esse histórico:
const app = createServer((request, response) => {
console.log(`Request method: ${request.method} | Endpoint: ${request.url}`);
// resto do código...
O nosso terminal ficará assim:
Server started at http://localhost:3000
Request method: GET | Endpoint: /links
Request method: GET | Endpoint: /outro-endpoint
Request method: GET | Endpoint: /outro-endpoint-2
Criando os outros endpoints
Antes de criar o resto dos endpoints, já percebeu que pra cada um a gente está precisando repetir isso?
response.writeHead(404, { "content-type": "application/json" });
response.end(JSON.stringify({ error: `Cannot ${request.method} ${request.url}` }));
Pois é — a gente pode melhorar essa experiência criando uma função helper que facilite isso. Vamos criar src/helpers/extend-response.js com uma função responsável por retornar o objeto response com os seguintes métodos utilitários:
.status(statusCode)— recebe um status code como parâmetro..json(body)— recebe um body como parâmetro.
// src/helpers/extend-response.js
export function extendResponse(response) {
response.status = function (statusCode) {
this.statusCode = statusCode;
return this;
};
response.json = function (body) {
response.setHeader("content-type", "application/json");
response.end(JSON.stringify(body));
return this;
};
return { response };
}
E pra usar essa função:
const app = createServer((request, res) => {
const { response } = extendResponse(res);
console.log(`Request method: ${request.method} | Endpoint: ${request.url}`);
const route = routes.find(
({ endpoint, method }) => endpoint === request.url && method === request.method
);
if (route) {
return route.handler(request, response);
}
response.status(404).json({ error: `Cannot ${request.method} ${request.url}` });
});
Importante: É necessário renomear o parâmetro do createServer para res para não ter conflito de variáveis.
Dica: O this como retorno dos métodos utilitários serve para que seja possível encadear os métodos, ex: .status().json(). Isso lembra bastante o Express :)
Ficou muito melhor né? Agora vamos atualizar nosso handler:
// src/controllers/link.controller.js
// ...
export default {
listLinks(request, response) {
response.status(200).json(links);
},
}
Endpoint para buscar um link pelo short_code
Pra fazer esse endpoint a gente só precisa criar o controller e adicionar mais uma rota:
// src/controllers/link.controller.js
// ...
export default {
listLinks(_, response) {...},
getLinkByShortCode(request, response) {
const { short_code } = request.params;
const link = links.find((link) => link.short_code === short_code);
if (!link) {
return response.status(400).json({ error: "Link not found" });
}
response.status(200).json(link);
},
}
// src/route.js
// ...
export default [
{...},
{
endpoint: "/links/:short_code",
method: "GET",
handler: linkController.getLinkByShortCode,
},
]
Seria muito lindo se fosse só isso né? Mas tem um garfo aí.
O que é :short_code? Pra quem veio do Express como eu, isso funciona como mágica. Mas a triste notícia é que parâmetros de URL dinâmicos não existem por padrão no Node.
E novamente isso não tem nada a ver com o Node em si — é algo que precisa ser implementado. E o que fazemos quando isso acontece? Vamos implementar!
Criando uma lógica para parâmetros de URL dinâmicos
Primeiro a observar é que request.params não existe. Pra adicioná-lo, vamos fazer o seguinte:
- Formatar a URL para capturar o
short_code - Como
:short_codeé um placeholder, vamos identificá-lo na URL - Se encontrado, adicionar um objeto
paramscom esseshort_code
import { extendResponse } from "./helpers/extend-response.js";
// ...
const BASE_URL = "http://localhost:3000";
const app = createServer((request, res) => {
const parsedUrl = new URL(BASE_URL + request.url);
const { response } = extendResponse(res);
let { pathname } = parsedUrl;
console.log(`Request method: ${request.method} | Endpoint: ${pathname}`);
let short_code = null;
const splitEndpoint = pathname.split("/").filter(Boolean);
if (splitEndpoint.length > 1) {
pathname = `/${splitEndpoint[0]}/:short_code`;
short_code = splitEndpoint[1];
}
const route = routes.find(
({ endpoint, method }) => endpoint === pathname && method === request.method
);
if (route) {
request.params = { short_code };
return route.handler(request, response);
}
response.status(404).json({ error: `Cannot ${request.method} ${pathname}` });
});
O processo funciona assim:
- Dividindo a URL: A URL é dividida em partes com
split("/"), transformando o caminho em um array de segmentos. Ex:/links/abc123→["links", "abc123"]. - Identificando o parâmetro dinâmico: Se o array tiver mais de um segmento, o código assume que o segundo é um parâmetro dinâmico. Ele ajusta o
pathnamepara um formato genérico como/links/:short_codee armazena o valor real (abc123) emshort_code. - Buscando a rota correspondente: O código procura na lista de rotas uma que corresponda ao
pathnameajustado e ao método HTTP. - Passando os parâmetros para o handler: O valor do parâmetro dinâmico é armazenado em
request.params, facilitando o acesso dentro do handler.
Muito loco né? E detalhe: isso funciona para qualquer novo endpoint que tiver um nível de parâmetro de URL dinâmico.
Criando o endpoint de criação de um link
Até agora temos apenas dois endpoints: listar todos os links e buscar um pelo short_code. Agora vamos adicionar o endpoint para criar um novo link.
// src/controllers/link.controller.js
import { randomUUID } from "node:crypto";
// ...
export default {
listLinks(request, response) {...},
getLinkByShortCode(request, response) {...},
createLink(request, response) {
const { body } = request;
const linkExists = links.find(
(link) => link.short_code === body.short_code
);
if (linkExists) {
return response.status(400).json({ error: "Short code already exists" });
}
const newLink = {
id: randomUUID(),
original_url: body.original_url,
short_code: body.short_code,
created_at: new Date().toISOString(),
};
links.push(newLink);
response.status(201).json(newLink);
},
}
// src/route.js
// ...
export default [
{...},
{...},
{
endpoint: "/links",
method: "POST",
handler: linkController.createLink,
},
]
E assim como o request.params, o request.body também não existe nativamente. 😂
Como funciona um body de uma requisição?

Quando trabalhamos com APIs, o body de uma requisição HTTP é onde os dados enviados pelo cliente são armazenados. Esses dados geralmente acompanham métodos como POST, PUT ou PATCH. Mas por que eles são tratados como streams?
O body de uma requisição HTTP pode estar em diferentes formatos:
- JSON:
{"name":"John","age":30} - Formulários:
name=John&age=30 - Texto puro:
Hello, world! - Arquivos binários: imagens, vídeos, etc.
No Node, o body de uma requisição HTTP não chega como um único bloco de dados. Em vez disso, ele é enviado como uma stream — os dados chegam ao servidor em pedaços (chunks) ao longo do tempo.
Com isso em mente, precisamos criar um bodyParser simples:
// src/helpers/body-parser.js
export function bodyParser(request, callback) {
let body = "";
if (!["POST", "PUT", "PATCH"].includes(request.method)) {
return callback();
}
request.on("data", (chunk) => {
body += chunk;
});
request.on("end", () => {
body = JSON.parse(body);
request.body = body;
callback();
});
}
O que ele está fazendo:
- Inicialização: Uma variável
bodyé criada para armazenar os dados recebidos. - Recebendo dados: O evento
dataé acionado sempre que um chunk chega. Esses pedaços são concatenados embody. - Finalizando a transmissão: Quando todos os dados foram recebidos, o evento
endé acionado e obodycompleto é convertido de JSON para um objeto JavaScript. - Callback: Após o processamento, o callback é chamado para continuar o fluxo da aplicação.
Por exemplo, ao fazer um POST com o body:
{
"name": "John Doe",
"age": 22
}
O servidor não recebe isso como um único bloco. Ele pode receber assim:
Primeiro chunk: { "name": "John
Segundo chunk: Doe", "age":
Terceiro chunk: 22 }
O bodyParser junta esses pedaços, monta o body completo e o transforma em um objeto JavaScript.
Usando o bodyParser no index.js
// src/index.js
import { bodyParser } from "./helpers/body-parser.js";
// ...
const app = createServer((request, res) => {
const parsedUrl = new URL(BASE_URL + request.url);
const { response } = extendResponse(res);
let { pathname } = parsedUrl;
console.log(`Request method: ${request.method} | Endpoint: ${pathname}`);
let short_code = null;
const splitEndpoint = pathname.split("/").filter(Boolean);
if (splitEndpoint.length > 1) {
pathname = `/${splitEndpoint[0]}/:short_code`;
short_code = splitEndpoint[1];
}
const route = routes.find(
({ endpoint, method }) => endpoint === pathname && method === request.method
);
if (route) {
request.params = { short_code };
return bodyParser(request, () => route.handler(request, response));
}
response.status(404).json({ error: `Cannot ${request.method} ${pathname}` });
});
E pronto, agora nosso endpoint de criação de link funciona! O retorno dele é:
{
"id": "78d09eaa-722c-4620-8e20-0fb76e8b3b25",
"original_url": "https://linkedin.com/in/wesleydmscn",
"short_code": "linkedin-wesley",
"created_at": "2025-04-05T20:30:30.650Z"
}
Criando o endpoint para excluir um link
// src/controllers/link.controller.js
// ...
export default {
listLinks(request, response) {...},
getLinkByShortCode(request, response) {...},
createLink(request, response) {...},
deleteLink(request, response) {
const { short_code } = request.params;
const link = links.find((link) => link.short_code === short_code);
if (!link) {
return response.status(400).json({ error: "Link not found" });
}
links = links.filter((link) => link.short_code !== short_code);
response.status(204).end();
},
}
// src/route.js
// ...
export default [
{...},
{...},
{...},
{
endpoint: "/links/:short_code",
method: "DELETE",
handler: linkController.deleteLink,
},
]
E pronto, se fizermos uma requisição DELETE para http://localhost:3000/links/linkedin-wesley, esse link será excluído do banco em memória.
Criando o endpoint principal: redirecionar para o link original
E por fim, vamos implementar o endpoint principal da nossa aplicação e, pra felicidade de vocês...
Não temos um response.redirect() haha.
Mas não esquenta, é mais simples do que as coisas que já vimos hoje.
Como funciona o redirecionamento HTTP?
Quando um servidor realiza um redirecionamento, ele instrui o cliente a acessar uma URL diferente. Para isso, dois elementos são essenciais:
1. Status code entre 300 e 399
Os códigos na faixa de 300–399 indicam ao cliente que o recurso pode ser encontrado em outro lugar:
- 301 (Moved Permanently): O recurso foi movido permanentemente.
- 302 (Found): O recurso está temporariamente em outra URL.
- 307 (Temporary Redirect): Similar ao 302, mas preserva o método HTTP.
- 308 (Permanent Redirect): Similar ao 301, mas também preserva o método HTTP.
2. Cabeçalho Location
O cabeçalho Location é obrigatório em respostas de redirecionamento. Ele informa ao cliente para qual URL ele deve ir. Sem ele, o cliente não saberá para onde redirecionar, mesmo que o status code esteja correto.
HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-url
Implementando o redirecionamento
Primeiro adicionamos o novo endpoint no arquivo de rotas:
// src/route.js
// ...
export default [
{...},
{...},
{...},
{...},
{
endpoint: "/r/:short_code",
method: "GET",
handler: linkController.redirectToLink,
},
];
Agora vamos estender a função extendResponse adicionando um método .redirect():
// src/helpers/extend-response.js
export function extendResponse(response) {
// ...
response.redirect = function (statusCode, location) {
response.writeHead(statusCode, { Location: location });
response.end();
};
// ...
}
E por fim, adicionar o método no controller:
// src/controllers/link.controller.js
// ...
export default {
listLinks(request, response) {...},
getLinkByShortCode(request, response) {...},
createLink(request, response) {...},
deleteLink(request, response) {...},
redirectToLink(request, response) {
const { short_code } = request.params;
const link = links.find((link) => link.short_code === short_code);
if (!link) {
return response.status(400).json({ error: "Link not found" });
}
response.redirect(301, link.original_url);
},
};
E pronto! Agora quando o usuário acessa a URL passando o short_code criado previamente, ele é redirecionado para o original_url.