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:
- name, referente ao nome do projeto.
- version, referente a versão.
- description, referente a descrição do projeto que está sendo criado.
- entry point, arquivo que será o ponto de entrada caso o projeto seja importado por outro.
- test command, comando que executará os testes de aplicação.
- git repository, repositório git do projeto.
- keywords, palavras chave para ajudar outros desenvolvedores a encontrar o seu projeto no npm.
- author, autor do projeto.
- 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.