Como criei meu próprio micro framework para node.js

Eu sei que existem vários frameworks para Node.js por aí: Express, Fastify, Adonis, NestJS, e por aí vai. Mesmo sabendo disso, decidi criar o meu próprio.

Não, eu não acho que ele seja melhor do que nenhum desses. Mas só o fato de já existirem tantas opções desanima muita gente que cogita criar o seu. E isso vale pra várias áreas, não só programação.

Os motivos são muitos: medo de não conseguir, achar que nunca vai superar os outros, comparação constante, entre outros…

Só que pensar assim te trava. Te impede de criar algo novo. E, sinceramente, eu não acho que isso faça bem.

Se você tem curiosidade e vontade de fazer alguma coisa, só faz. Porque, se não der o primeiro passo, nunca vai sair do lugar.

Eu criei esse micro framework com o objetivo de aprender, entender, de verdade, o que acontece por baixo dos panos.

Vou passar por cada parte do código pra você entender exatamente como tudo foi construído.

Inicialmente importei o método createServer. Ele é o centro de tudo, onde você vai iniciar seu servidor HTTP. Abaixo dele, existem outros metodos de tipagem: IncomingMessage, ServerResponse e Server pra sabermos o que cada função retorna, todos importados do node:http

src/index.ts

import {
createServer,
IncomingMessage,
ServerResponse,
Server,
} from "node:http";

//...

Depois de importar o que eu precisava, criei minhas próprias interfaces e tipos pra definir o que cada função vai receber e retornar. Isso deixa tudo mais claro e evita dor de cabeça.

src/index.ts

//...

type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";

type FrameworkResponse =
// ServerResponse &
{
  message: (value: string) => void;
  json: (value: unknown) => void;
};

type RouteHandler = (
req: IncomingMessage,
res: FrameworkResponse
) => void | Promise<void>;

interface Route {
path: string;
method: HTTPMethod;
handler: RouteHandler;
}

//...

Depois de tipar tudo, criei a classe Framework. Ela é a alma do projeto.

Dentro dela, temos duas propriedades privadas: routes e server. A primeira guarda as rotas registradas. A segunda instancia o servidor HTTP que vai receber as requisições.

No constructor, eu uso createServer passando como argumento o método handleRequest, já com o .bind(this) pra garantir que ele continue acessando o contexto certo da classe.

src/index.ts

//...

export class Framework {
private routes: Route[] = [];
private server: Server;

constructor() {
  this.server = createServer(this.handleRequest.bind(this));
}

//...
}

E aí entra um dos pontos mais importantes: a função serverResponse. Ela serve pra adaptar a resposta original do Node (que é um pouco crua) e dar uma interface mais amigável pra gente usar. Com ela, posso responder com texto usando message() ou com JSON via json().

src/index.ts

export class Framework {
//...

private serverResponse(response: ServerResponse): FrameworkResponse {
  return {
    message: (body) => {
      response.end(body);
    },
    json: (body) => {
      response.end(JSON.stringify(body));
    },
    // ...response
  } as FrameworkResponse;
}

//...
}

O método handleRequest é o coração de tudo. Ele é chamado toda vez que chega uma requisição. Nele, extraímos o método e a URL da request, procuramos se existe alguma rota registrada com essa combinação e, se existir, executamos o handler dela.

Caso a rota não exista, respondemos com um { message: "Not found" }.

src/index.ts

export class Framework {
//...

private async handleRequest(
  request: IncomingMessage,
  response: ServerResponse
): Promise<void> {
  const frameWorkResponse = this.serverResponse(response);

  const { method, url } = request;

  const route = this.routes.find(
    (currentRoute) =>
      currentRoute.method === method && currentRoute.path === url
  );

  if (route) {
    await route.handler(request, frameWorkResponse);
  } else {
    frameWorkResponse.json({ message: "Not found" });
  }
}

//...
}

Depois disso, criei uma função chamada registerRoute pra centralizar a lógica de adicionar rotas no array routes.

Com ela pronta, ficou fácil criar os métodos públicos: get, post, put e delete. Cada um só chama registerRoute com o método respectivo.

src/index.ts

export class Frameworks {
//...

private registerRoute(
  method: HTTPMethod,
  path: string,
  handler: RouteHandler
) {
  this.routes.push({ method, path, handler });
}

public get(path: string, handler: RouteHandler): void {
  this.registerRoute("GET", path, handler);
}

public post(path: string, handler: RouteHandler): void {
  this.registerRoute("POST", path, handler);
}

public put(path: string, handler: RouteHandler): void {
  this.registerRoute("PUT", path, handler);
}

public delete(path: string, handler: RouteHandler): void {
  this.registerRoute("DELETE", path, handler);
}

//...
}

E por fim, o método listen, que inicia o servidor e escuta em uma porta. Simples e direto.

src/index.ts

export class Framework {
//...

public listen(port: number, callback?: () => void): void {
  this.server.listen(port, callback);
}
}

E pronto! Com isso, você tem um micro framework funcional, leve e 100% feito por você.

O que eu mais gosto nesse tipo de exercício é a clareza que ele proporciona. Depois de fazer algo do zero, usar ferramentas prontas como Express ou Fastify faz ainda mais sentido, porque você passa a entender os porquês por trás de cada decisão.

Não se trata de reinventar a roda. Se trata de aprender construindo.

Então se você ainda tá com aquela ideia engavetada por achar que "alguém já fez melhor", ignora isso por um instante. Vai lá e faz. Porque só tentando é que você realmente aprende.

Seu projeto não precisa ser revolucionário. Só precisa ser seu.

Ficou interessado no projeto? Ele é open source! Dá uma olhada no repositório clicando no botão aqui embaixo.

GitHub