Como funciona o Express JS? Criando um HTTP Server "express like" do zero, sem frameworks
Tags: javascript, node, tutorial, dev
Published: 2020-08-22 19:59:50

## Introdução
Na [**primeira parte**](https://dev.to/thiagomr/frameworks-js-ame-os-ou-deixe-os-31fc) deste artigo, fiz uma abordagem com prós e contras sobre o uso (ou não uso) de frameworks. Apesar de ter grandes vantagens, tem um ponto negativo que me chama bastante atenção no dia-a-dia: muitos desenvolvedores estão completamente dependentes de frameworks, ao ponto de não conhecer alguns dos fundamentos básicos da tecnologia com a qual trabalham. Como já citei anteriormente, eu defendo bastante o “do the basics”, ter o conhecimento da base faz com que você tenha uma visão mais ampla e consequentemente, implementações melhores para suas soluções.

Hoje eu vou trazer a implementação de um **HTTP Server** inspirado no [**Express**](https://github.com/expressjs), feito completamente do zero, sem uso de frameworks. Ao longo do caminho vou mostrar alguns exemplos bem comuns no universo Node JS, como **high order functions**, **chaining functions** e **events**. Em paralelo, vou trazer a implementação com o próprio Express também para fazermos um comparativo. No final do artigo, vou deixar o link com todos os exemplos no **Github**. Então, vamos nessa!

>**Nota**: *De forma alguma a intenção deste artigo é refazer o express ou criar um framework que substitua o mesmo. Os exemplos são totalmente didáticos, visando o entendimento de alguns conceitos, não tendo a intenção de rodar em produção ou ser um robusto framework de servidor http.*

Primeiramente nós precisamos de um nome legal, então para fazer uma alusão ao express, vamos chamar nosso módulo de **Rockspress**.


## Conhecendo o módulo HTTP

Antes de iniciarmos o desenvolvimento do módulo em si, vou demonstrar como criar um simples http server em NodeJS (acreditem, muitos desenvolvedores nunca tiveram a necessidade de criar um). No fim das contas, todos os frameworks que fazem abstração de servidor HTTP utilizam o módulo nativo http (ou https). Segue exemplo:

```js
//http.js
const http = require('http');

http.createServer((req, res) => {
    res.write('done!');
    res.write('\n');
    res.end();
}).listen(8000, () => console.log('server listen at 8000'));

```

Primeiramente chamamos a função ***createServer***, que retorna um callback com um objeto para o ***request*** (que contém todas as informações do que é recebido a cada requisição ao servidor, como url, body, headers, etc) e outro para o ***response*** (que contém toda a estrutura para retornar ao cliente uma resposta no padrão http, como status code, body, headers, etc). Em seguida utilizamos a função **listen** para deixar o servidor disponível na porta 8000. No nosso exemplo estamos apenas escrevendo uma mensagem no corpo do response e finalizando o mesmo. Agora, vamos de fato ao nosso módulo.


## Iniciando o módulo Rockspress
A primeira coisa que faremos é criar uma classe e no método construtor, criaremos uma propriedade que vai receber nosso server nativo, conforme o exemplo anterior:

```js
//rockspress.js
const http = require(‘http’);

class Rockspress {
    constructor() {
        this.server = http.createServer();
    }
}
```


## Criando uma estrutura de dados

Em seguida, criaremos uma estrutura de dados, que vai ser responsável por receber e armazenar nossas funções de acordo com o roteamento. Escolhi utilizar uma estrutura de chave / valor:

```js
class Rockspress {
    constructor() {
        this.server = http.createServer();

        this.router = {
            GET: {},
            POST: {},
            PUT: {},
            DEL: {}
        };
    }
} 
```
Criamos um objeto chamado **router**, que vai conter uma propriedade para cada método http que iremos implementar. Por sua vez, cada uma das dessas propriedades também é um objeto.

## Registrando novas rotas

Agora que já temos umas estrutura para armazenar os dados, vamos criar uma função para registrar novas rotas de acordo com o método http escolhido. Vamos utilizar uma **high order function** (para quem não conhece o termo, é uma estratégia bem comum em programação funcional, se tratando uma função que opera outra função, recebendo como argumento ou retornando como saída). Não vou entrar muito a fundo nos detalhes, pois isso renderia um artigo inteiro. No nosso caso a intenção é fazer uma abstração de um método que recebe um parâmetro predefinido e parâmetros inseridos por quem chama a função.
```js
registerRoute(method) {
    return function (route, callback) {
        this.router[method][route] = callback;
    }
}
 ```
 
A abstração recebe o método http, a rota escolhida e qual função será executada e salva essas informações de acordo com nossa estrutura de dados.
 
## Fazendo o handle dos requests
Agora nós precisamos redirecionar os requests recebidos de acordo com a rota e método escolhidos. O módulo **http** nativo do Node, é todo baseado em eventos. Sempre que recebemos uma nova requisição no nosso servidor, é emitido um evento chamado **request**. Vamos criar uma função que vai fazer o gerenciamento desse evento, utilizando as propriedades **req.method** e **req.url** para acessar nossa estrutura de dados, enviando como argumentos os objetos de **request** e **response**.

```js
class Rockspress {
    constructor() {
        //...

        this.server.on('request', this.handleRequest.bind(this));
    }

    async handleRequest(req, res) {
        if (!this.router[req.method][req.url]) {
            res.statusCode = 404;
            res.write('not found');
            return res.end();
        }

        this.router[req.method][req.url](req, res);
    }
}
```
  
Adicionamos mais uma abstração que vai verificar se o método e rota solicitados existem. Caso negativo, será retornada uma resposta padrão com o statusCode 404 (not found).

Agora que já estamos fazendo o roteamento das requisições recebidas para seus devidos métodos, precisamos alterar os objetos de request e response, colocando algumas abstrações adicionais para tornar o uso mais amigável.


## Alterando o objeto Response

Vamos começar com o **response**. Criaremos uma função que recebe o response original e adicionar dois métodos no mesmo. O primeiro, seta o **statusCode** e o segundo escreve no **responseBody** um argumento recebido. Os dois retornam a própria **response**, permitindo assim o uso de um padrão bastante comum em javascript chamado **chaining functions**, que consiste em chamar múltiplas funções consecutivas a partir mesmo objeto.
  
```js
setupResponse(response) {
    response.status = (statusCode) => {
        response.statusCode = statusCode;
        return response;
    }

    response.send = (responseBody) => {
        response.write(responseBody);
        response.end();
    }

    return response;
}
```

Exemplo de chamada com chaining function:

```js
res.status(200).send('ok');
```
  
Nesse momento, já somos capazes de receber requisições, rotear para seus devidos metódos e retornar uma resposta. Mas antes de testar tudo funcionando, iremos implementar uma abstração para o objeto do **request**, permitindo pegar o que foi enviado como body da requisição.

## Alterando o objeto Request

O recebimento do corpo da requisição, também é feito via evento, o mesmo é um **stream** que vem em partes. O que faremos é juntar as partes dessa **stream**, colocando tudo em uma propriedade do request em forma de json, para um acesso mais fácil (simulando algo semelhante ao que o middleware **body-parser** faz).

```js
setupRequest(request) {
    request.body = '';

    request.on('data', chunk => {
        request.body += chunk.toString();
    });

    return new Promise(resolve => request.on('end', () => {
        request.body = request.body ? JSON.parse(request.body) : '';
        resolve(request);
    }));
}
```

Assim que o **request** recebe todas as partes, fazemos o parse para JSON e retornamos o request já alterado.
Vamos adicionar nossos métodos de setup de request e response no nosso handler de requests. Esse setup também poderia ser feito por eventos, porém, escolhi utilizar promises para facilitar o entendimento (É necessário colocar a keyword **async** no mesmo, pois estamos chamando uma promessa utilizando a keyword **await**).

```js
async handleRequest(req, res) {
    req = await this.setupRequest(req);
    res = this.setupResponse(res);

    if (!this.router[req.method][req.url]) {
        res.statusCode = 404;
        res.write('not found');
        return res.end();
    }

    this.router[req.method][req.url](req, res);
}
```

## Expondo o servidor em uma porta

É necessário também expor uma porta onde o server vai escutar as requisições enviadas. Para isso, faremos uma simples abstração do método **listen** do módulo http, apenas passando pra frente os mesmos argumentos recebidos:

```js
listen() {
    const args = Array.prototype.slice.call(arguments);
    return this.server.listen.apply(this.server, args);
}
```

## Refatorando
Para finalizar, vamos refatorar o código, dando mais clareza na leitura, criando funções que abstraem a criação da estrutura de dados e configuração dos métodos. Também iremos exportar uma instância do mesmo. Com isso, nosso módulo completo ficará da seguinte forma:

```js
//rockspress.js
const http = require('http');

class Rockspress {
    constructor() {
        this.server = http.createServer();
        this.setupRoutes();
        this.setupMethods();
        this.server.on('request', this.handleRequest.bind(this));
    }

    setupRoutes() {
        this.router = {
            GET: {},
            POST: {},
            PUT: {},
            DEL: {}
        };
    }

    setupMethods() {
        this.get = this.registerRoute('GET');
        this.post = this.registerRoute('POST');
        this.put = this.registerRoute('PUT');
        this.del = this.registerRoute('DEL');
    }

    async handleRequest(req, res) {
        req = await this.setupRequest(req);
        res = this.setupResponse(res);

        if (!this.router[req.method][req.url]) {
            res.statusCode = 404;
            res.write('not found');
            return res.end();
        }

        this.router[req.method][req.url](req, res);
    }

    setupRequest(request) {
        request.body = '';

        request.on('data', chunk => {
            request.body += chunk.toString();
        });

        return new Promise(resolve => request.on('end', () => {
            request.body = request.body ? JSON.parse(request.body) : '';
            resolve(request);
        }));
    }

    setupResponse(response) {
        response.status = (statusCode) => {
            response.statusCode = statusCode;
            return response;
        }

        response.send = (responseBody) => {
            response.write(responseBody);
            response.end();
        }

        return response;
    }

    registerRoute(method) {
        return function (route, callback) {
            this.router[method][route] = callback;
        }
    }

    listen() {
        const args = Array.prototype.slice.call(arguments);
        return this.server.listen.apply(this.server, args);
    }
}

module.exports = new Rockspress();

```

## Implementando o HTTP Server

E agora vamos criar a implementação do mesmo, com rotas GET e POST:

```js
//index.js
const rockspress = require('./rockspress');

rockspress.get('/', (req, res) => {
    return res.status(200).send('main');
});

rockspress.get('/ping', (req, res) => {
    return res.status(200).send('pong');
});

rockspress.post('/send', (req, res) => {
    console.log('request body', req.body);

    return res.status(200).send('sent');
});

rockspress.get('/error', (req, res) => {
    return res.status(500).send('error');
});

rockspress.listen(8000, () => console.log('listen at 8000'));

```

## Testando as rotas implementadas
Para testar, vamos utilizar o comando **curl** (pode-se utilizar qualquer client http desejado). Primeiro vamos inicializar app:

```bash
node index.js
#listen at 8000
```

Rota GET: 
```bash
curl http://localhost:8000/ping
#pong
```
Rota POST:
```bash
curl -X POST http://localhost:8000/send -H 'Content-Type: application/json' --data '{"foo":"bar"}'
##request body {"foo":"bar"}
##sent
```

E para finalizar, a implementação lado a lado com express e com nosso rockspress:

```js
//both.js

/**
 * ------------------------------------------------------------------------------
 *  Server with express framework
 */

const express = require('express');
const app = express();

app.use(express.json());

app.get('/', (req, res) => {
    return res.status(200).send('main');
});

app.get('/ping', (req, res) => {
    return res.status(200).send('pong');
});

app.post('/send', (req, res) => {
    console.log('request body', req.body);

    return res.status(200).send('sent');
});

app.get('/error', (req, res) => {
    return res.status(500).send('error');
});

app.listen(8000, () => console.log('listen at 8000'));


/**
 * ------------------------------------------------------------------------------
 *  Server with custom framework
 */


const rockspress = require('./rockspress');

rockspress.get('/', (req, res) => {
    return res.status(200).send('main');
});

rockspress.get('/ping', (req, res) => {
    return res.status(200).send('pong');
});

rockspress.post('/send', (req, res) => {
    console.log('request body', req.body);

    return res.status(200).send('sent');
});

rockspress.get('/error', (req, res) => {
    return res.status(500).send('error');
});

rockspress.listen(8001, () => console.log('listen at 8001'));

```

Segue o link do **github** com todos os exemplos: https://github.com/thiagomr/rockspress


## Conclusão

E assim vamos chegando ao fim. Além de aprender diversos conceitos da linguagem, é possível também perceber o quanto é trabalhoso implementar o mínimo de funcionalidades (mesmo que para fins didáticos, sem se preocupar com performance, dentre outros aspectos importantes) de um framework como o express. Eu recomendo que vocês façam esse tipo de engenharia reversa, pra mim funciona como uma ótima fonte de aprendizado e para os novatos pode ser realmente esclarecedor, afinal, quem nunca se perguntou *"mas de onde vem esse req, res"*?
 
Então é isso, espero que tenham gostado. Me sigam no [**twitter**](https://twitter.com/_thiagomrs) para ficarem ligados nos proximos artigos e compartilhem com os amigos para que eu possa continuar gerando conteúdo. Feedbacks, comentários e sugestões sempre são bem-vindos.

Um grande abraço e até a próxima!