Iniciando o projeto

Para iniciar um projeto em Node.js a primeira coisa a fazer é inicializar o npm no diretório onde ficará a aplicação. Para isso, primeiro certifique-se de já ter instalado o Node.js e o npm em seu computador, caso ainda não os tenha vá até o site do Node.js e faça o download https://nodejs.org/en/download/. Ele irá instalar automaticamente também o npm.

Configuração inicial

Crie o diretório onde ficará sua aplicação, após isso, dentro do diretório execute o seguinte comando:

1 $ npm init

Semelhante ao git, o npm inicializará um novo projeto nesse diretório, depois de executar o comando o npm vai apresentar uma série de perguntas (não é necessário respondê-las agora, basta pressionar enter. Você poderá editar o arquivo de configuração depois) como:

  1. name, referente ao nome do projeto.
  2. version, referente a versão.
  3. description, referente a descrição do projeto que está sendo criado.
  4. entry point, arquivo que será o ponto de entrada caso o projeto seja importado por outro.
  5. test command, comando que executará os testes de aplicação.
  6. git repository, repositório git do projeto.
  7. keywords, palavras chave para ajudar outros desenvolvedores a encontrar o seu projeto no npm.
  8. author, autor do projeto.
  9. license referente a licença de uso do código.

Após isso um arquivo chamado package.json será criado com o conteúdo semelhante a:

 1 {
 2   "name": "node-book",
 3     "version": "1.0.0",
 4     "description": "",
 5     "main": "index.js",
 6     "scripts": {
 7       "test": "echo \"Error: no test specified\" && exit 1"
 8     },
 9     "author": "",
10     "license": "ISC"
11 }

O package.json é responsável por guardar as configurações npm do nosso projeto, nele ficarão nossos scripts para executar a aplicação e os testes.

Configurando suporte ao Ecmascript 6

Como vimos anteriormente o Babel sera responsável por nos permitir usar as funcionalidades do ES6, para isso precisamos instalar os pacotes e configurar o nosso ambiente para suportar o ES6 por padrão em nossa aplicação. O primeiro passo é instalar os pacotes do Babel:

1 $ npm install --save-dev @babel/cli@^7.7.4 @babel/core@^7.7.4 @babel/node@^7.7.4

Após instalar o Babel é necessário instalar o preset que será usado, no nosso caso será o ES6:

1 $ npm install --save-dev @babel/preset-env@^7.7.4

Note que sempre usamos --save-dev para instalar dependências referentes ao Babel pois ele não deve ser usado diretamente em produção, para produção vamos compilar o código, veremos isso mais adiante.

O último passo é informar para o Babel qual preset iremos usar, para isso basta criar um arquivo no diretório raiz da nossa aplicação chamado .babelrc com as seguintes configurações:

 1 {
 2   "presets": [
 3     [
 4       "@babel/preset-env",
 5       {
 6         "targets": {
 7           "node": true
 8         }
 9       }
10     ]
11   ]
12 }

Feito isso a aplicação já estará suportando 100% o ES6 e será possível utilizar as funcionalidades da versão. O env informa ao Babel pela ler o ambiente de desenvolvimento e adicionar as funcionalidades que estão faltando para a versão do Node.js que esta sendo utilizada. O código dessa etapa está disponivel neste link.

Configurando o servidor web

Como iremos desenvolver uma aplicação web precisaremos de um servidor que nos ajude a trabalhar com requisições HTTP, transporte de dados, rotas e etc. Existem muitas opções no universo Node.js como o Sails.js, Hapi.js e Koa.js. Vamos optar pelo Express.js por possuir um bom tempo de atividade, muito conteúdo na comunidade e é mantido pela Node Foundation.

O Express é um framework para desenvolvimento web para Node.js inspirado no Sinatra que é uma alternativa entre outros frameworks web para a linguagem ruby. Criado por TJ Holowaychuk o Express foi adquirido pela StrongLoop em 2014 e é administrado atualmente pela Node.js Foundation.

Para começar será necessário instalar dois modulos: o express e o body-parser. Conforme o exemplo a seguir:

1 $ npm install express@^4.14.0 body-parser@^1.15.2

Quando uma requisição do tipo POST ou PUT é realizada, o corpo da requisição é transportado como texto. Para que seja possível transportar dados como JSON (JavaScript Object Notation) por exemplo existe o modulo body-parser que é um conjunto de middlewares para o express que analisa o corpo de uma requisição e transforma em algo definido, no nosso caso, em JSON.

Agora vamos criar um arquivo chamado server.js no diretório raiz e nele vamos fazer a configuração básica do express:

 1 import express from 'express';
 2 import bodyParser from 'body-parser';
 3 
 4 const app = express();
 5 app.use(bodyParser.json());
 6 
 7 app.get('/', (req, res) => res.send('Hello World!'));
 8 
 9 app.listen(3000, () => {
10   console.log('Example app listening on port 3000!');
11 });

A primeira instrução no exemplo acima é a importação dos módulos express e body-parser que foram instalados anteriormente. Em seguida uma nova instância do express é criada e associada a constante app. Para utilizar o body-parser é necessário configurar o express para utilizar o middleware, o express possui um método chamado use onde é possível passar middlewares como parâmetro, no código acima foi passado o bodyParser.json() responsável por transformar o corpo das requisições em JSON.

A seguir é criada uma rota, os verbos HTTP como GET, POST, PUT, DELETE são funções no express que recebem como parâmetro um padrão de rota, no caso acima /, e uma função de callback que será chamada quando a rota receber uma requisição. Os parametros req e res representam request (requisição) e response (resposta) e serão injetados automaticamente pelo express quando a requisição for recebida. Para finalizar, a função listen é chamada recebendo um número referente a porta na qual a aplicação ficará exposta, no nosso caso é a porta 3000.

O último passo é configurar o package.json para iniciar nossa aplicação, para isso vamos adicionar um script de start dentro do objeto scripts:

1   "scripts": {
2     "build": "babel src --out-dir dist",
3     "start": "npm run build && node dist/server.js",
4     "start:dev": "babel-node src/server.js",
5     "test": "echo \"Error: no test specified\" && exit 1"
6   },

### Build O Comando build utiliza o Babel para transpilar o código que foi escrito em Javascript suportado pelo ambiente, por exemplo se voce está utilizando Node na versão 12 e utilizando imports (ES6 Modules) o Babel vai transformar essa funcionalidade em algo suportado pela versão 12. Note que o código depois de transpilado será salvo no diretório dist. Esse código que deve ser utilizado em produção.

Start

O start é indicado para utilizar em produção, ele vai executar o comando build antes e depois iniciar a aplicação a partir do código transpilado.

Start dev

A diferença do start e do start:dev e que o start:dev utilizada o babel-node que transpila e inicia a aplicação ao mesmo tempo, vamo adicinar mais coisas a esse comando logo.

Alterado o package.json basta executar o comando:

1 $ npm start

Agora a aplicação estará disponível em http://localhost:3000/. O código dessa etapa está disponivel neste link.

Entendendo o Middleware pattern

Note esta etapa não faz parte do código da API que estamos construindo, o código é somente exemplo de implementação.

O padrão de Middleware implementado pelo express já é bem conhecido e tem sido usado por desenvolvedores em outras linguagens há muitos anos. Podemos dizer que se trata de uma implementação do padrão intercepting filter pattern do chain of responsibility.

A implementação representa um pipeline de processamento onde handlers, units e filters são funções. Essas funções são conectadas criando uma sequência de processamento assíncrona que permite pré-processamento, processamento e pós-processamento de qualquer tipo de dado.

Uma das principais vantagens desse pattern é a facilidade de adicionar plugins de maneira não intrusiva.

O diagrama abaixo representa a implementação do Middleware pattern:

O primeiro componente que devemos observar no diagrama acima é o Middleware Manager, ele é responsável por organizar e executar as funções. Alguns dos detalhes mais importantes dessa implementação são:

Novos middlewares podem ser invocados usando a função `use() (o nome não precisa ser estritamente use, aqui estamos usando o express como base). Geralmente novos middlewares são adicionados ao final do pipeline, mas essa não é uma regra obrigatória. Quando um novo dado é recebido para processamento, o middleware registrado é invocado em um fluxo de execução assíncrono. Cada unidade no pipeline recebe o resultado da anterior como input. Cada pedaço do middleware pode decidir parar o processamento simplesmente não chamando o callback, ou em caso de erro, passando o erro por callback. Normalmente erros disparam um fluxo diferente de processamento que é dedicado ao tratamento de erros.

O exemplo abaixo mostra um caminho de erro:

No express, por exemplo, o caminho padrão espera os parâmetros request, response e next, caso receba um quarto parâmetro, que normalmente é nomeado como error, ele vai buscar um caminho diferente.

Não há restrições de como os dados são processados ou propagados no pipeline. Algumas estratégias são:

  • Incrementar os dados com propriedades ou funções.
  • Substituir os dados com o resultado de algum tipo de processamento.
  • Manter a imutabilidade dos dados sempre retornando uma cópia como resultado do processamento.

A implementação correta depende de como o Middleware Manager é implementado e do tipo de dados que serão processados no próprio middleware. Para saber mais sobre o pattern sugiro a leitura do livro Node.js Design Patterns.

Middlewares no Express

O exemplo a seguir mostra uma aplicação express simples, com uma rota que devolve um “Hello world” quando chamada:

 1 const express = require('express');
 2 
 3 const app = express();
 4 
 5 app.get('/', function(req, res, next) {
 6 	console.log('route / called');
 7   res.send('Hello World!');
 8 });
 9 
10 app.listen(3000, () => {
11 	console.log('app is running');
12 });

Agora vamos adicionar uma mensagem no console que deve aparecer antes da mensagem da rota:

 1 const express = require('express');
 2 
 3 const app = express();
 4 
 5 app.use((req, res, next) => {
 6   console.log('will run before any route');
 7   next();
 8 });
 9 
10 app.get('/', function(req, res, next) {
11 	console.log('route / called');
12   res.send('Hello World!');
13 });
14 
15 app.listen(3000, () => {
16 	console.log('app is running');
17 });

Middlewares são apenas funções que recebem os parâmetros requisição (req), resposta (res) e próximo (next), executam alguma lógica e chamam o próximo middleware chamando next. No exemplo acima chamamos o use passando uma função que será o middleware, ela mostra a mensagem no console e depois chama o next().

Se executarmos esse código e acessarmos a rota / a saída no terminal será:

1 app is running
2 will run before any route
3 route / called

Ok! Mas como eu sabia que iria executar antes? Como vimos anteriormente no middleware pattern, o middleware manager executa uma sequência de middlewares, então a ordem do use interfere na execução, por exemplo, se invertermos a ordem, como no código abaixo:

 1 const express = require('express');
 2 
 3 const app = express();
 4 
 5 
 6 app.get('/', function(req, res, next) {
 7 	console.log('route / called');
 8   res.send('Hello World!');
 9 });
10 
11 app.use((req, res, next) => {
12 	console.log('will run before any route');
13 	next();
14 });
15 
16 app.listen(3000, () => {
17 	console.log('app is running');
18 });

A saida será:

1 app is running
2 route / called

Dessa vez o nosso middleware não foi chamado, isso acontece porque a rota chama a função res.send() invés de next(), ou seja, ela quebra a sequência de middlewares.

Também é possível usar middlewares em rotas específicas, como abaixo:

 1 const express = require('express');
 2 
 3 const app = express();
 4 
 5 app.use('/users', (req, res, next) => {
 6 	console.log('will run before users route');
 7 	next();
 8 });
 9 
10 app.get('/', function(req, res, next) {
11 	console.log('route / called');
12   res.send('Hello World!');
13 });
14 
15 app.get('/users', function(req, res, next) {
16 	console.log('route /users called');
17 	res.send('Hello World!');
18 });
19 
20 app.listen(3000, () => {
21 	console.log('app is running');
22 });

Caso seja feita uma chamada para / a saída sera:

1 app is running
2 route / called

Já para /users veremos a seguinte saída no terminal:

1 app is running
2 will run before users route
3 route /users called

O express também possibilita ter caminhos diferentes em caso de erro:

 1 const express = require('express');
 2 
 3 const app = express();
 4 
 5 app.use((req, res, next) => {
 6 	console.log('will run before any route');
 7 	next();
 8 });
 9 
10 app.use((err, req, res, next) => {
11 	console.log('something goes wrong');
12 	res.status(500).send(err.message);
13 });
14 
15 app.get('/', function(req, res, next) {
16 	console.log('route / called');
17   res.send('Hello World!');
18 });
19 
20 
21 app.listen(3000, () => {
22 	console.log('app is running');
23 });

Não mudamos nada no código de exemplo, apenas adicionamos mais um middleware que recebe o parâmetro err. Executando o código teremos a seguinte saída:

1 app is running
2 will run before any route
3 route / called

Apenas o primeiro middleware foi chamado, o middleware de erro não. Vamos ver o que acontece quando passamos um erro para o next do primeiro middleware.

 1 const express = require('express');
 2 
 3 const app = express();
 4 
 5 app.use((req, res, next) => {
 6 	console.log('will run before any route');
 7 	next(new Error('failed!'));
 8 });
 9 
10 app.use((err, req, res, next) => {
11 	console.log('something goes wrong');
12 	res.status(500).send(err.message);
13 });
14 
15 app.get('/', function(req, res, next) {
16 	console.log('route / called');
17   res.send('Hello World!');
18 });
19 
20 
21 app.listen(3000, () => {
22 	console.log('app is running');
23 });

A saída será:

1 app is running
2 will run before any route
3 something goes wrong

Essas são as formas mais comuns de utilizar middlewares, mas suas combinações são infinitas. Ao decorrer do livro utilizaremos middlewares para casos distintos.