Construindo Aplicações Com NodeJS - 3 Edição - William Bruno Moraes - 2021
Construindo Aplicações Com NodeJS - 3 Edição - William Bruno Moraes - 2021
Construindo Aplicações Com NodeJS - 3 Edição - William Bruno Moraes - 2021
Novatec
© Novatec Editora Ltda. 2018, 2021.
Todos os direitos reservados e protegidos pela Lei 9.610 de 19/02/1998. É proibida a reprodução desta
obra, mesmo parcial, por qualquer processo, sem prévia autorização, por escrito, do autor e da Editora.
Editor: Rubens Prates GRA20210327
Revisão gramatical: Mônica d'Almeida
Editoração eletrônica: Carolina Kuwabata
Capa: Carolina Kuwabata
ISBN do impresso: 978-65-86057-53-9
ISBN do ebook: 978-65-86057-54-6
Histórico de impressões:
Abril/2021 Terceira edição
Junho/2018 Segunda edição (ISBN: 978-85-7522-685-8)
Outubro/2015 Primeira edição (ISBN: 978-85-7522-456-4)
Novatec Editora Ltda.
Rua Luís Antônio dos Santos 110
02460-000 – São Paulo, SP – Brasil
Tel.: +55 11 2959-6529
Email: [email protected]
Site: https://novatec.com.br
Twitter: twitter.com/novateceditora
Facebook: facebook.com/novatec
LinkedIn: linkedin.com/company/novatec-editora/
Aos meus pais – Catarina e Carlos –,
sem os quais eu não teria conseguido nada na vida.
Sumário
Agradecimentos
Sobre o autor
Prefácio
capítulo 1 Introdução
1.1 JavaScript
1.1.1 Variáveis
1.1.2 Comentários
1.1.3 Funções
1.1.4 Operações
1.1.5 Controles de fluxo
1.1.6 Laços de repetição
1.1.7 Coleções
1.1.8 Template strings
1.1.9 Destructuring assignment
1.1.10 Spread
1.1.11 Rest parameters
1.1.12 Optional chaining
1.1.13 Default argument
1.1.14 use strict
1.1.15 ESlint
1.2 Instalação do NodeJS
1.2.1 Módulos n e nvm
1.2.2 Arquivo package.json
1.3 NPM (Node Package Manager)
1.3.1 npm update
1.3.2 npx
1.3.3 yarn (Package Manager)
1.4 Console do NodeJS (REPL)
1.4.1 Variáveis de ambiente
1.5 Programação síncrona e assíncrona
1.5.1 Promises
1.5.2 async/await
1.6 Orientação a eventos
1.7 Orientação a objetos
1.7.1 TypeScript
1.8 Programação funcional
1.9 Tenha um bom editor de código
1.9.1 Arquivo de preferências do Sublime Text 3
1.9.2 Arquivo de preferências do Visual Studio Code
1.9.3 EditorConfig.org
1.10 Plugin para visualização de JSON
capítulo 6 FrontEnd
6.1 Arquivos estáticos
6.2 Client side
6.2.1 xhr
6.2.2 fetch
6.2.3 jQuery
6.2.4 ReactJS
6.3 Server side
6.3.1 Nunjucks
6.3.2 Handlebars
6.3.3 Pug
6.3.4 React Server Side
capítulo 8 Produção
8.1 Healthcheck
8.1.1 /check/version
8.1.2 /check/status
8.1.3 /check/status/complete
8.2 APM (Aplication Performance Monitoring)
8.3 Logs
8.4 forever e pm2
8.5 Nginx
8.5.1 compression
8.5.2 Helmet
8.6 Docker
8.7 MongoDB Atlas
8.8 AWS
8.8.1 unix service
8.8.2 Nginx
8.8.3 aws-cli
8.9 Heroku
8.10 Travis CI
8.11 GitHub Actions
Referências bibliográficas
Agradecimentos
Convenções utilizadas
Para facilitar a explicação do conteúdo e sua leitura, as seguintes tipografias
foram utilizadas neste livro:
Fonte maior
Indica nomes de arquivos.
Itálico
Indica hiperlinks e URIs (Uniform Resource Identifier).
Negrito
Indica nomes de módulos, projetos, conceitos, linguagens ou número de
versão.
Monoespaçada
No meio do texto indica alguma variável ou arquivo, indica uma
combinação de teclas, uma sequência de menus para algum programa ou
um trecho de código.
Monoespaçada em negrito
Destaca um trecho de código para o qual quero chamar sua atenção.
1.1 JavaScript
JavaScript (comumente abreviado para JS) é uma linguagem de programação
de alto nível, leve, dinâmica, multi-paradigma, não tipada e interpretada.
Originalmente desenvolvida pelo Mestre Jedi Brendan Eich
(https://brendaneich.com) em apenas dez dias. O JavaScript completou 25
anos em 2020 e tem sido uma das linguagens mais utilizadas no mundo desde
então, o que impulsionou o seu crescimento, amadurecimento e uso em larga
escala.
A sintaxe do JavaScript foi baseada na linguagem C, enquanto a semântica e
o design vieram das linguagens Self e Scheme. O que torna o JavaScript
incrivelmente poderoso e flexível é a possibilidade de utilizar diversos
paradigmas de programação combinados em uma só linguagem. Após ter
sido muito criticado e desacreditado, o uso de JS só aumentou conforme a
evolução da própria web. Como diria Brendan Eich: “Always bet on JS”.
Vemos na Figura 1.2 o slide de uma palestra dele.
1.1.1 Variáveis
A linguagem JavaScript possui sete tipos de dados primitivos:
• Boolean – true ou false
• Number – um número, tanto inteiro quanto float, é do tipo Number.
Exemplos: 1, -15, 9,9
• BigInt – criado para representar inteiros grandes arbitrários. Exemplos:
> BigInt(1234567890)
1234567890n
> typeof 10n
'bigint'
• String – usado para representar um texto. Exemplos:
> String('qq coisa')
'qq coisa'
• Symbol – tipo de dado em que as instâncias são únicas e imutáveis.
Exemplos:
> const kFoo = Symbol('kFoo')
undefined
> typeof kFoo
'symbol'
• undefined – indica um valor não definido, ou seja, algo que não foi ainda
atribuído a nada. Exemplos:
> let notDefined
undefined
> notDefined === undefined
true
• null – palavra-chave especial para um valor nulo. Exemplos:
> const amINull = null
undefined
> typeof amINull
'object'
> amINull === null
true
E tipos estruturais:
• Object – tipo estrutural do qual todos os objetos derivam. Exemplos:
> typeof {}
'object'
> typeof new String()
'object'
• Function – representa funções. Exemplos:
> typeof (() => {})
'function'
> typeof (new function())
'function'
> typeof (function(){})
'function'
Apenas null e undefined não têm métodos; todos os outros tipos podem ser
utilizados como objetos ou convertidos neles.
Para declarar uma nova variável, basta indicar o nome após a palavra
reservada var, let ou const e atribuir um valor com um símbolo de igualdade:
var creator_name = 'George Lucas';
let year = 1977;
const saga = 'Star Wars';
• var – declara uma variável com escopo de função;
• let – declara uma variável com escopo de bloco;1
• const – declara uma constante, ou seja, algo que não pode ter seu valor
atribuído novamente.
Escopo de função
O escopo de função permite o comportamento conhecido por closure, por
meio do qual as variáveis definidas em um escopo acima também são
acessíveis em um escopo mais específico, ou seja, numa função interna.
> (function(){
var arr = [];
function something(){ console.log(arr); }
arr.push(1);
arr.push(2);
something();
})();
[ 1, 2 ]
Nesse exemplo, a função something() teve acesso à variável arr, que foi
declarada um escopo acima do seu.
Escopo de bloco
O let que tem escopo por bloco cria novas referências para cada bloco:
> var out = 'May 25, 1977';
> let out2 = 'Jun 20, 1980';
> if (true) { var out = 'May 25, 1983'; let out2 = 'May 19, 1999'; }
> out;
'May 25, 1983'
> out2
'Jun 20, 1980'
O fato interessante a notar é que a variável out que está fora do bloco if teve o
seu valor alterado, enquanto a out2 não. Ela permaneceu com o valor inicial
declarado fora do bloco, enquanto outro espaço de memória foi alocado para
a out2 de dentro do bloco do if. Desse comportamento, temos que let não faz
hoisting,2 enquanto o var faz.
Um bloco sendo definido pelo código entre chaves {}:
> { let someVar = 2 }
undefined
> someVar
Uncaught ReferenceError: someVar is not defined
const
Atente ao fato de que, declarando um array ou objeto como const, podemos
alterar os valores internos dele, mas não a referência em si:
> const arr = []
undefined
> arr.push(1)
1
> arr = 2
Uncaught TypeError: Assignment to constant variable.
Apenas fazendo um Object.freeze, teremos um array imutável:
> Object.freeze(arr)
[1]
> arr.push(2)
Uncaught TypeError: Cannot add property 1, object is not extensible
at Array.push (<anonymous>)
O mesmo vale para objetos:
> const obj = {}
undefined
> obj.owner = 'Disney'
'Disney'
> obj
{ owner: 'Disney' }
> obj = 'Lucas Films'
Uncaught TypeError: Assignment to constant variable.
De agora em diante, não utilizaremos mais a palavra reservada var, preferindo
sempre usar const; somente quando precisarmos reatribuir valores, usaremos
let.
1.1.2 Comentários
A linguagem aceita comentários de linha e de bloco, que são instruções
ignoradas pelo interpretador. A função é destacar ou explicar um trecho de
código ao programador que estiver lendo o código-fonte.
> //comentário de linha
>
> /*
comentário de bloco
*/
1.1.3 Funções
Funções no JavaScript podem ser declaradas, atribuídas, passadas por
referência ou retornadas, por isso dizemos que elas são objetos (cidadãos) de
primeira classe.
Existem algumas formas diferentes de declarar uma função:
function bar(){}
const foo = function() {}
const foo = () => {}
(function(){})
(() => {})()
Tendo em vista a possibilidade de criar uma função sem nome e o escopo
baseado em funções, conseguimos criar uma closure com uma função
anônima autoexecutável (IIFE – Immediately-Invoked Function Expression).
(function(){
var princess = 'Leia'
})()
console.log(princess)
Uncaught ReferenceError: princess is not defined
Ou usando arrow functions:
(() => {
var princess = 'Leia'
})()
console.log(princess)
A variável princess não existe fora da IIFE, mas a IIFE pode acessar qualquer
variável que tenha sido declarada fora dela.
Neste caso o escopo de var é limitado a IIFE, não fazendo hoisting para fora.
Arrow function
Arrow function ou Fat Arrow function é uma sintaxe alternativa à
declaração das funções com a palavra reservada function. Não possui seu
próprio this e não pode ser usada como função construtora. Por exemplo, a
seguinte função para contar a quantidade de caracteres de cada item do
array pode ser escrita dessa forma:
> const studios = ['20th Century Fox', 'Warner Bros.', 'Walt Disney Pictures'];
undefined
> studios.map(function(s) { return s.length; });
[ 16, 12, 20 ]
Ou, utilizando arrow function, ficaria dessa forma:
> const studios = ['20th Century Fox', 'Warner Bros.', 'Walt Disney Pictures'];
undefined
> studios.map(s => s.length);
[ 16, 12, 20 ]
Vamos preferir arrow function a function de agora em diante no livro, quando
for cabível.
1.1.4 Operações
Os operadores numéricos são +, -, *, / e %, para adição, subtração,
multiplicação, divisão e resto, respectivamente. Os valores são atribuídos
com um operador de igualdade. O operador + também concatena strings, o
que é um problema, pois podemos tentar somar números com strings e obter
resultados esquisitos.
> 1 + 3 + '2'
'42'
Comparações são feitas com dois ou três sinais de igualdade. A diferença é
que == (dois iguais) comparam valores, fazendo coerção de tipo, podendo
resultar que 42 em string seja igual a 42 number.
> '42' == 42
true
Entretanto, com três operadores de igualdade, o interpretador não converte
nenhum dos tipos e faz uma comparação que só responde true caso sejam
idênticos tanto o valor quanto o tipo.
> '42' === 42
false
Da mesma forma que o operador ponto de exclamação, ou negação (!),
inverte o valor, ele pode ser usado para comparar se uma coisa é diferente da
outra, comparando != ou !==. É recomendado que sempre se utilize a
comparação estrita === ou !==.
Dois operadores de negação convertem um valor para o seu booleano:
> !!''
false
> !!'a'
true
Podemos também combinar a atribuição com os operadores +, -, *, / e %,
tornando possível resumir uma atribuição e alteração de valor numa sintaxe
mais curta:
> let one = 1;
> one = one + 1;
2
> var one = 1;
> one += 1;
2
Ou, então, com duplo ++ ou --, para incremento ou decremento:
> let one = 1;
> one++
1
> one = one++;
2
Ainda existem os operadores de bit &, |, ^, ~, <<, >> etc., mas não vou explicá-
los profundamente aqui.
O operador && (logical AND) diz que, para algo ser verdade, ambos os lados
da expressão devem ser verdadeiros.
> true && true
true
O operador || (logical OR) adiciona OU à expressão, possibilitando que
qualquer um dos lados da expressão seja verdade, para o resultado ser
verdadeiro.
> false || true
true
Podemos resumir a expressão deste if ternário:
> const noTry = false ? false : 'do not';
> noTry
'do not'
Em:
> const noTry = false || 'do not';
> noTry;
'do not'
O operador ?? (nullish coalescing) somente executa a segunda parte se e
somente se a operação da esquerda retornar null, diferente do || que executa
para qualquer valor que seja entendido como falso:
> null ?? 'valor padrão'
'valor padrão'
> true ?? 'valor padrão'
true
O operador && (logical AND) executa a segunda parte da expressão se a
primeira for verdade, senão, retorna à primeira:
> true && console.log('The Mandalorian')
The Mandalorian
undefined
> false && console.log('Han Solo')
false
Loop while
> const arr = [1,2,3,5,7,11];
> const i = 0;
> const max = arr.length;
> while(i < max) {
console.log(arr[i]);
i++;
}
map
> const arr = [1,2,3,5,7,11];
> arr.map(x => console.log(x))
forEach
> const arr = [1,2,3,5,7,11];
> arr.forEach(x => console.log(x))
Loop for in
> const arr = [1,2,3,5,7,11];
> for (x in arr) {
console.log(x);
}
1.1.7 Coleções
Iniciando pela estrutura mais simples que temos para representar um conjunto
de dados, os arrays são estruturas de dados que lhe permitem colocar uma
lista de valores em uma única variável. Os valores podem ser qualquer tipo
de dado, seja String, Number, Object ou misto.
Array
Array de números:
> const arr = [];
> arr.push(1);
> arr.push(2);
> arr.push(3);
> arr
[ 1, 2, 3 ]
Array de String:
> const arr = [];
> arr.push('a');
> arr.push('b');
> arr.push('c');
> arr
[ 'a', 'b', 'c' ]
Array de objetos:
> const arr = [];
> arr.push({ name: 'William' });
> arr.push({ name: 'Bruno' });
> arr
[ { name: 'William' }, { name: 'Bruno' } ]
> arr.length
2
Existem também Typed Arrays que são arrays tipados, por isso são mais
performáticos para manipular dados binários brutos, utilizados na
manipulação de áudio, vídeo e WebSockets. Exemplo:
> const arr = new Uint8Array([21,31])
undefined
> arr
Uint8Array(2) [ 21, 31 ]
JSON
Outra estrutura com a qual estamos bem acostumados é a Notação de Objeto
do JavaScript, ou JSON, como comumente conhecemos. Utilizamos na
comunicação entre aplicações (APIs REST, mensagens em filas,
armazenagem em banco de dados etc.).
Consiste na sintaxe de objeto literal, como a seguinte:
{
"title": "Construindo aplicações com NodeJS",
"author": {
"name": "William Bruno"
},
"version": 3,
"tags": ["javascript", "nodejs"],
"ebook": true
}
Set
Set são coleções que permitem armazenar valores únicos de qualquer tipo. Por
conta dessa característica, é uma forma muito prática de remover duplicidade
de um array.
> const arr = [1,2,2,3,3,3,4,4,4,4]
undefined
> arr
[
1, 2, 2, 3, 3,
3, 4, 4, 4, 4
]
> new Set(arr)
Set(4) { 1, 2, 3, 4 }
Agora temos um objeto Set de números únicos e podemos transformar
novamente em array usando spread syntax.
> [...new Set(arr)]
[ 1, 2, 3, 4 ]
Vale lembrar que, ao comparar dois objetos com ===, eles somente serão
iguais se representarem o mesmo objeto, no mesmo endereço de memória;
caso contrário, o resultado será sempre falso.
> { a: 1 } === { a: 1 }
false
A forma correta de comparar objetos em JavaScript é comparar atributo a
atributo, e o NodeJS possui um método utilitário para tal.
> const util = require('util')
undefined
> util.isDeepStrictEqual({ a: 1 }, { a: 1 })
true
Map
Map são coleções únicas identificadas por uma chave. Em ES5, simulávamos
esse comportamento, com a notação literal de objetos:
const places = {
'Coruscant': 'Capital da República Galática',
'Estrela da Morte': 'Estação espacial com laser capaz de explodir outros planetas',
'Dagobah': 'Lar do Mester Yoda',
'Hoth': 'Congelado e remoto',
'Endor': 'Florestas de Ewoks',
'Naboo': 'Cultura exótica',
'Tatooine': 'Dois sóis'
}
Acessando as propriedades:
> Object.keys(places).length
7
> !!places['Naboo']
true
Hoje em dia, podemos usar o operador new Map(), em que o primeiro
argumento do método set é a chave, e o segundo é o valor.
const places = new Map()
places.set('Coruscant', 'Capital da República Galática')
places.set('Estrela da Morte', 'Estação espacial com laser capaz de explodir outros
planetas')
places.set('Dagobah', 'Lar do Mester Yoda')
places.set('Hoth', 'Congelado e remoto')
places.set('Endor', 'Florestas de Ewoks')
places.set('Naboo', 'Cultura exótica')
places.set('Tatooine', 'Dois sóis')
> places.size
7
> places.has('Tatooine')
true
Podemos alterar um valor de uma chave:
> places.get('Naboo')
'Cultura exótica'
> places.set('Naboo', 'Rainha Amidala')
> places.get('Naboo')
'Rainha Amidala'
Ou remover:
> places.delete('Naboo', 'Rainha Amidala')
true
Outros dois tipos de coleções são WeakSet e WeakMap, usados para guardar
referências de objetos, durante verificações em loop ou recursivas.
> const ws = new WeakSet()
> ws.add({ composer: 'Ludwig Göransson', age: 36 })
WeakSet { <items unknown> }
1.1.8 Template strings
Não há diferença entre uma string declarada entre aspas duplas e aspas
simples, apesar de, por convenção, sempre utilizarmos apenas uma dessas
duas formas. Ainda assim, a interpolação de strings com variáveis era algo
bem verboso antes da ECMAScript 6 (https://nodejs.org/en/docs/es6/):
> const name = 'Padmé';
> const message = 'Oi, ' + name + ' !';
> message
'Oi, Padmé !'
Com a chegada dos template strings, utilizamos crases em vez de aspas e
interpolamos variáveis usando ${}:
> const name = 'Padmé';
> const message = `Oi, ${name} !`;
> message
'Oi, Padmé !'
Sem uso do operador de adição (+):
> const name = 'Qui-Gon Jinn'
> `Oi, ${name} !`;
'Oi, Qui-Gon Jinn !'
Outra facilidade proporcionada é que strings de múltiplas linhas podem ser
escritas sem necessidade de concatenar ou escapar linha a linha, por exemplo:
> const aNewHope = `
It is a period of civil war. Rebel spaceships, striking from a hidden base, have won their
first victory against the evil Galactic Empire. During the battle, Rebel spies managed to
steal secret plans to the Empire's ultimate weapon, the DEATH STAR, an armored space
station with enough power to destroy an entire planet.
Pursued by the Empire's sinister agents, Princess Leia races home aboard her starship,
custodian of the stolen plans that can save her people and restore freedom to the galaxy...
`
> console.log(aNewHope)
1.1.10 Spread
O operador spread permite expandir arrays ou objetos, fazendo cópias destes
para outros destinos.
Dado o objeto:
const televisionSerie = {
title: 'The Mandalorian',
createdBy: {
name: 'Jon Favreau',
birth: '1966-10-19',
country: 'U.S'
},
starring: [
{ name: 'Pedro Pascal', birth: '1975-04-02', country: 'Chile' }
]
}
Antes fazíamos uma cópia de um objeto para outro utilizando Object.assign:
> const target = {}
undefined
> Object.assign(target, televisionSerie)
> target
{
title: 'The Mandalorian',
createdBy: { name: 'Jon Favreau', birth: '1966-10-19', country: 'U.S' },
starring: [ { name: 'Pedro Pascal', birth: '1975-04-02', country: 'Chile' } ]
}
Mas, usando spread, o código fica mais declarativo:
> const copy = { ...televisionSerie }
undefined
> copy
{
title: 'The Mandalorian',
createdBy: { name: 'Jon Favreau', birth: '1966-10-19', country: 'U.S' },
starring: [ { name: 'Pedro Pascal', birth: '1975-04-02', country: 'Chile' } ]
}
Para fazer merge de objetos, os valores da direita têm preferência:
> const jonFavreau = { name: 'Jonathan Kolia Favreau' }
undefined
> { ...televisionSerie.createdBy, ...jonFavreau }
{ name: 'Jonathan Kolia Favreau', birth: '1966-10-19', country: 'U.S' }
E com arrays:
> const season1 = [
'Dave Filoni',
'Rick Famuyiwa',
'Deborah Chow',
'Bryce Dallas Howard',
'Taika Waititi'
]
> const season2 = [
'Jon Favreau',
'Peyton Reed',
'Carl Weathers',
'Robert Rodriguez',
]
> const directories = [
...season1,
...season2
]
> directories
[
'Dave Filoni',
'Rick Famuyiwa',
'Deborah Chow',
'Bryce Dallas Howard',
'Taika Waititi',
'Jon Favreau',
'Peyton Reed',
'Carl Weathers',
'Robert Rodriguez'
]
Existem outras aplicações para transformar um array em uma lista de
argumentos:
> console.log(...directories)
Dave Filoni Rick Famuyiwa Deborah Chow Bryce Dallas Howard Taika Waititi Jon
Favreau Peyton Reed Carl Weathers Robert Rodriguez
1.1.15 ESlint
JavaScript não é uma linguagem compilada, por isso frequentemente
programadores que tiveram experiências anteriores em outras linguagens,
como C# ou Java, não se sentem seguros por não serem avisados, em tempo
de compilação, sobre erros de sintaxe ou erros de digitação. Os lints são
ferramentas que verificam esses tipos de falha, além de ajudar a melhorar a
qualidade do código ao validar boas práticas de desenvolvimento.
O JSHint (http://jshint.com) é um desses lints. Ele foi inspirado no JSLint
(http://www.jslint.com), original do Douglas Crockford. Hoje em dia,
utilizamos o ESLint (https://eslint.org), devido ao grande número de regras e
suporte a JSX.
Dentre as regras mais importantes, um lint verifica situações, como:
• erros de sintaxe;
• padronização do código entre o time de desenvolvedores;
• argumentos, variáveis ou funções não utilizados;
• controle do número de globais;
• complexidade e aninhamento máximos;
• blocos implícitos que devem ser definidos com chaves;
• comparações não estritas.
Instale o ESlint (https://github.com/eslint/eslint) como dependência de
desenvolvimento nos seus projetos e como pacote global para poder usar o
command line tool:
$ npm install eslint --save-dev; npm install --global eslint
Após executar o comando eslint --init e responder algumas perguntas:
$ eslint --init
ü How would you like to use ESLint? · style
ü What type of modules does your project use? · commonjs
ü Which framework does your project use? · none
ü Does your project use TypeScript? · No / Yes
ü Where does your code run? · No items were selected
ü How would you like to define a style for your project? · guide
ü Which style guide do you want to follow? · google
ü What format do you want your config file to be in? · JSON
ü Would you like to install them now with npm? · No / Yes
Um arquivo .eslintrc.json será criado na raiz do projeto.3
$ cat .eslintrc.json
{
"env": {
"commonjs": true,
"es2021": true
},
"extends": [
"google"
],
"parserOptions": {
"ecmaVersion": 12
},
"rules": {
}
}
Dentro da sessão rules, conseguimos customizar as regras e os padrões que
queremos verificar e assegurar com o lint. Feito isso, podemos executar no
nosso projeto e até pedir para o ESlint corrigir alguns erros mais comuns,
informando a flag --fix.
$ eslint index.js
$ eslint index.js --fix
0.0.x - PATCH
Identifica versões com correções de bugs, patchs ou pequenas melhorias,
conhecidas como patch ou bug fixes.
0.x.0 - MINOR
Identifica versões com novas funcionalidades, mas sem quebrar a
compatibilidade com as demais versões anteriores. Conhecidas como
minor, breaking ou features.
x.0.0 - MAJOR
Identifica grandes alterações; quando esse número se altera, indica que a
versão atual não é compatível com a anterior. Conhecidas como major. O
primeiro release em que o major é igual a 1 indica que a API está estável, e
não se esperam grandes mudanças que quebrem os clientes por certo tempo.
O formato do versionamento semântico é: MAJOR.MINOR.PATCH.
As versões mais novas (números mais altos) são retrocompatíveis com as
versões anteriores. Então, códigos escritos em versões v0.10.x, v0.12.x, ou
v4.0.x funcionam normalmente nas versões mais atuais, mas o contrário não
é verdade, pois algumas novas features só existem nas versões em que foram
lançadas e posteriores.
Um exemplo disso é o operador var, hoje em dia, com o ótimo suporte a
ECMAScript 6 que o NodeJS tem, devido à versão da v8 que ele utiliza, não
usamos mais var no nosso código, e sim let ou const.
Desse momento em diante, é importante que a versão do NodeJS instalada na
sua máquina seja superior a v14.0.0, pois utilizaremos diversas features ES6
a seguir.
Scripts do package.json
No arquivo package.json existe uma seção chamada scripts. Nessa seção do
JSON configuramos atalhos para comandos que queremos executar e
definimos como a aplicação reage aos comandos padrão do npm, como start e
test. Podemos também criar comandos personalizados, como, por exemplo, o
comando forrest.
"scripts": {
"forrest": "echo 'Run, Forrest, run!'",
"test": "echo \"Error: no test specified\" && exit 1"
},
Editado o package.json, executamos o nosso comando com npm run
<nomecomando>:
$ npm run forrest
> [email protected] forrest /Users/wbruno/Sites/livro-nodejs
> echo 'Run! Forrest, run!'
Run! Forrest, run!
Não iremos nos preocupar com o comando test por enquanto. Ao longo do
livro, adicionaremos mais scripts nessa seção. É possível executar qualquer
comando bash com os scripts do package.json.
Conforme avançamos no projeto e escrevemos mais scripts, a tendência é
ficar cada vez mais difícil dar manutenção, pois fica ilegível uma linha shell
com múltiplas operações escritas como texto:
"scripts": {
"dev": "export DEBUG=livro_nodejs:* &&nodemon server/bin/www",
"test": "NODE_OPTIONS=--experimental-vm-modules jest server/**/*.test.js
tests/**/*.test.js --coverage --forceExit --detectOpenHandles"
},
Felizmente, são apenas comandos bash, logo podemos exportar para um
arquivo .sh! Para isso, liberamos permissão de execução com +x:
$ chmod +x scripts.sh
Simplificamos o package.json:
"scripts": {
"dev": "./scripts.sh dev",
"test": "./scripts.sh test"
},
E criamos um arquivo .sh para ter toda a lógica e organização que queremos:
Arquivo scripts.sh
#!/bin/bash
case "$(uname -s)" in
Darwin)
echo 'OS X'
OS='darwin'
;;
Linux)
echo 'Linux'
OS='linux'
;;
*)
echo 'Unsupported OS'
exit 1
esac
case "$1" in
dev)
export DEBUG=livro_nodejs:*
nodemon server/bin/www
;;
test)
export NODE_OPTIONS=--experimental-vm-modules
jest server/**/*.test.js tests/**/*.test.js --coverage --forceExit --detectOpenHandles
;;
build)
echo 'Building...'
rm -rf node_modules dist
mkdir -p dist/
npm install
;;
*)
echo "Usage: {dev|test|build}"
exit 1
;;
esac
Estamos executando o shell script informando um argumento ./scripts.sh dev,
então lemos esse argumento dentro do arquivo .sh, com $1. Dessa forma,
conseguimos pular linhas em vez de usar concatenadores de comandos como
; ou &&.
Outra coisa a notar, são os hooks pre e post. Conseguimos executar comandos
antes e depois de outros, como:
"preforrest": "echo 'Antes'",
"forrest": "echo 'Run, Forrest, run!'",
"postforrest": "echo 'Depois'",
Ficando a execução:
$ npm run forrest
> [email protected] preforrest …/livro-nodejs/capitulo_1/1.2.2
> echo 'Antes'
Antes
> [email protected] forrest …/livro-nodejs/capitulo_1/1.2.2
> echo 'Run, Forrest, run!'
Run, Forrest, run!
> [email protected] postforrest …/livro-nodejs/capitulo_1/1.2.2
> echo 'Depois'
Depois
package-lock.json ou yarn-lock.json
Os arquivos package-lock.json ou yarn-lock.json irão aparecer após você instalar o
primeiro módulo npm no projeto. Eles servem para garantir a instalação das
versões exatas das dependências que você usou no projeto; dessa forma, ao
executar num CI para deploy, terá garantias de que o que você desenvolveu
localmente será o mesmo na máquina de produção; portanto, é importante
fazer commit desse arquivo.
Lado bom
O lado “bom” do npmjs é que existem muitos módulos à disposição. Então,
sempre que você tiver que fazer alguma coisa em NodeJS, pesquise em:
https://www.npmjs.com para saber se já existe algum módulo que faça o que
você quer, ou uma parte do que você precisa, agilizando, assim, o
desenvolvimento da sua aplicação.
Lado ruim
O lado “ruim” do npmjs é que existem muitos módulos à disposição! Pois é,
acontece que, por ser completamente público e colaborativo (open source),
você encontrará diversos módulos com o mesmo propósito ou que realizam
as mesmas tarefas. Cabe a nós escolher aquele que melhor nos atenda. Para
isso, é importante seguir alguns passos:
• procure um módulo que esteja sendo mantido (com atualizações
frequentes, olhe a data do último commit);
• verifique se outros desenvolvedores estão utilizando o módulo (olhe o
número de estrelas, forks, issues abertas e fechadas etc.);
• é importante que ele contenha uma boa documentação dos métodos
públicos e da forma de uso, assim você não precisará ler o código-fonte
do projeto para realizar uma tarefa simples. Ler o código-fonte pode ser
interessante quando você tiver um tempo para isso ou precisar de alguma
otimização de baixo nível;
• procure testes de performance em que são comparados módulos
alternativos.
Assim você foca na sua aplicação e no desenvolvimento da regra de negócio.
1.3.2 npx
O npx (https://github.com/npm/npx) foi incorporado ao npm e é um command
line tool destinado a executar módulos do registry npm, mas sem
necessariamente instalá-los globalmente, como fazíamos antigamente com
pacotes como express-generator, create-react-app, react-native para só depois
executá-los.
Ele instala o módulo numa pasta temporária – se já não estiver instalado no
projeto local, ou no node_modules global –, executa o comando e depois
remove a biblioteca, liberando o espaço do disco.
Em vez de fazer:
$ npm install -g express-generator
$ express
Agora, usando npx, fazemos:
$ npx express-generator
Outro exemplo, executando o módulo cowsay:4
$ npx cowsay "Eu, Luke Skywalker, juro por minha honra e pela fé da irmandade dos
Cavaleiros, usar a Força apenas para o bem, negando, e ignorando sempre o Lado
Sombrio; dedicar minha vida à causa da liberdade e da justiça. Se eu falhar neste voto,
minha vida será perdida, aqui e no futuro."
Irá aparecer no terminal um desenho ASCII conforme mostrado na Figura
1.4.
Figura 1.4 – Resultado no terminal do comando cowsay.
1.5.1 Promises
Uma promise (https://promisesaplus.com) é a representação de uma operação
assíncrona. Algo que ainda não foi completado, mas é esperado que será num
futuro. Uma promise (promessa) é algo que pode ou não ser cumprido.
Utilizando corretamente, conseguimos diminuir o nível de encadeamento,
tornando o nosso código mais legível.
Essa é uma das técnicas que utilizaremos para evitar o famoso Callback Hell
(http://callbackhell.com).
Para declarar uma promise, usamos a função construtora Promise.
> new Promise(function(resolve, reject) {});
Promise { <pending> }
O retorno é um objeto promise que contém os métodos .then(), .catch() e finally().
Quando a execução tiver algum resultado, o .then() será invocado (resolve).
Quando acontecer algum erro, o .catch() será invocado (reject), e em ambos os
casos o .finally() será invocado, evitando assim a duplicação de código.
Qualquer exceção disparada pela função que gerou a promise ou pelo código
dentro do .then() será capturada pelo método.catch(), tendo assim um try/catch
implícito.
Além disso, é possível retornar valores síncronos para continuar encadeando
novos métodos .then(), mais ou menos assim:
> p1
.then(cb1)
.then(cb2)
.then(cb3)
.catch(cbError)
O método Promise.all() recebe como argumento um array de promises, e o seu
método .then() é executado quando todas elas retornam com sucesso.
Note que utilizar new Promise no meio do código é um anti-pattern e deve ser
evitado (https://runnable.com/blog/common-promise-anti-patterns-and-how-
to-avoid-them). Dentro do pacote util do core do NodeJS, temos o método
promisify (https://nodejs.org/api/util.html#util_util_promisify_original), que
recebe uma função que aceita um callback como último argumento e retorna
uma versão que utiliza promise.
Para isso, esse callback deve estar no padrão de que o primeiro argumento é o
erro, e os seguintes são os dados (err, value) => {} .
O código anterior que escreve um arquivo txt e depois realiza a leitura dele
fica dessa forma utilizando promises:
Arquivo writeFile.js
const fs = require('fs')
const promisify = require('util').promisify
const text = 'Star Wars (Brasil: Guerra nas Estrelas /Portugal: Guerra das Estrelas) é uma
franquia do tipo space opera estadunidense criada pelo cineasta George Lucas, que conta
com uma série de nove filmes de fantasia científica e dois spin-offs.\n'
const writeFileAsync = promisify(fs.writeFile)
const readFileAsync = promisify(fs.readFile)
writeFileAsync('promise.txt', text)
.then(_ => readFileAsync('promise.txt'))
.then(data => console.log(data.toString()))
Note que, em comparação com a versão anterior do código, não temos mais
dois níveis de aninhamento, pois estamos retornando uma promise dentro do
primeiro then e pegando o resultado no mesmo nível da função assíncrona
anterior.
No caso específico do módulo fs, já existe no core um novo módulo chamado
fs/promises, removendo a necessidade de usar o util.promisify:
Arquivo writeFile-promises.js
const fs = require('fs/promises')
const text = 'Star Wars (Brasil: Guerra nas Estrelas /Portugal: Guerra das Estrelas) é uma
franquia do tipo space opera estadunidense criada pelo cineasta George Lucas, que conta
com uma série de nove filmes de fantasia científica e dois spin-offs.\n'
fs.writeFile('promise.txt', text)
.then(_ => fs.readFile('async-await.txt'))
.then(data => console.log(data.toString()))
Note como o require vem de fs/promises.
1.5.2 async/await
Uma outra forma de trabalhar com promises é utilizar as palavras async/await.
O async transforma o retorno de uma função em uma promise.
Veja a seguinte função, com a palavra async no início da declaração:
async function sabre() {
return 'espada laser';
}
sabre().then(r => console.log(r))
Executando:
$ node sabre.js
espada laser
Para utilizar await em “top level”, ou seja, fora de uma função, é preciso
definir dynamic imports, declarando
"type": "modules", no package.json:
{
"type": "module"
}
Ou, então, usar arquivos .mjs (module).
Já que a função sabre agora retorna uma promise, devido ao async, podemos
usar await para aguardar o retorno, e o nosso código agora fica mais parecido
com um código imperativo, no qual eu consigo atribuir o retorno a uma
variável, e a ordem de execução e o retorno são exatamente a ordem em que o
código foi escrito.
async function sabre() {
return 'espada laser';
}
const r = await sabre()
console.log(r)
Executando:
$ node sabre.mjs
espada laser
Retornando ao exemplo de código que escreve o arquivo txt, utilizando
async/await, fica assim:
Arquivo writeFile.js
import fs from 'fs/promises'
const text = 'Star Wars (Brasil: Guerra nas Estrelas /Portugal: Guerra das Estrelas) é uma
franquia do tipo space opera estadunidense criada pelo cineasta George Lucas, que conta
com uma série de nove filmes de fantasia científica e dois spin-offs.\n'
await fs.writeFile('async-await.txt', text)
const data = await fs.readFile('async-await.txt')
console.log(data.toString())
Não foi necessário usar callbacks nem encaixar .then. Também tive que trocar
o require por import, utilizando dynamic imports, por ter habilitado o uso de
módulos no package.json, e então ser possível utilizar top level await.
Entendendo o prototype
Todos os objetos no JavaScript são descendentes de Object, e todos os objetos
herdam métodos e propriedades de Object.prototype. Esses métodos e essas
propriedades podem ser sobrescritos. Dessa forma, conseguimos simular o
conceito de herança, além de outras características interessantes do prototype.
Observe o objeto criado com a função construtora Droid.
> function Droid() {}
> const c3po = new Droid()
> Droid.prototype.getLanguages = function() { return this.languages; }
> Droid.prototype.setLanguages = function(n) { this.languages = n; }
> c3po.setLanguages(6_000_000)
> c3po.getLanguages()
6000000
Podemos atribuir métodos ou propriedades no prototype de Droid, e as
instâncias desse objeto herdarão essas propriedades mesmo que tenham sido
instanciadas antes de o método ter sido definido, assim como as novas
instâncias também herdarão esses métodos:
> const r2d2 = new Droid()
> r2d2.setLanguages(1)
> r2d2.getLanguages()
1
Para usar o protótipo para herdar de outros objetos, basta atribuir uma
instância do objeto base no prototype do objeto em que queremos receber os
métodos e as propriedades:
> function BattleDroid() {}
> BattleDroid.prototype = Object.create(Droid.prototype)
> const b1 = new BattleDroid()
> b1.setLanguages(1)
> b1.getLanguages()
1
No código anterior, utilizei função construtora, mas podería ter utilizado a
palavra-chave class, que é uma novidade da ECMAScript 6. Uma class
(https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Classes)
é apenas outra forma de criar objetos, o que chamamos de syntactic sugar,
para o prototype BattleDroid.
class Droid {
#languages
setLanguages(languages) {
this.#languages = languages
}
getLanguages() {
return this.#languages
}
}
> const c3po = new Droid()
undefined
> c3po.setLanguages(6_000_000)
6000000
> c3po.getLanguages()
6000000
Podemos escrever números grandes com _ (underline) entre os números para
facilitar a leitura, fazendo, por exemplo, a separação dos milhares, deixando
mais visível que 6000000 é 6 milhões, ao escrever 6_000_000.
1.7.1 TypeScript
O Typescript (https://www.typescriptlang.org) foi criado para dar definições
estáticas de tipo ao JavaScript, ao descrever a forma de um objeto, retorno de
métodos e parâmetros, melhorando a documentação e permitindo que seu
código seja validado em tempo de desenvolvimento na IDE (VSCode,
WebStorm etc.).
Em 2018, Ryan Dahl (o mesmo que criou o NodeJS em 2009) apresentou o
Deno (https://deno.land), como sendo um novo runtime para JavaScript e
TypeScript, tirando a necessidade de compilar TypeScript em JavaScript
antes de executar no NodeJS.
Frequentemente, utilizam TypeScript mais voltado ao paradigma de
orientação a objetos, para assim tirar maior proveito da tipagem.
Callback
Um callback é uma função passada como parâmetro de outra função, para
ser executada mais tarde, quando algum processo acabar. O fato de
conseguir passar uma função como parâmetro de outra já indica um suporte
à programação funcional (HOF).
Imutabilidade
Uma vez atribuído um valor a uma variável, ela nunca terá o seu valor
reatribuído; em vez disso, somos encorajados a retornar novas instâncias.
Currying
Esta é uma técnica que consiste em transformar uma função de n
argumentos em outra com menos ou com argumentos mais simples.
Monads
É uma forma de encapsular um valor em um contexto, provendo assim
métodos para fazer operações com o valor original.
Pipes
Um design pattern que descreve computação como uma série de etapas.
Trabalha com composição de funções, em que a próxima função continua a
partir do resultado da anterior.
Memoization
Memoization (http://addyosmani.com/blog/faster-javascript-memoization/)
é um padrão que serve para cachear valores já retornados, fazendo com que
a próxima resposta seja mais rápida. Dentre os problemas que o
memoization resolve, podemos citar cálculos matemáticos recursivos, cache
de algum algoritmo ou qualquer problema que possa ser expresso, como
chamadas consecutivas a uma mesma função com uma combinação de
argumentos.
Lazy Evaluation
O conceito de avaliação tardia consiste em atrasar a execução até que o
resultado realmente seja necessário. Dessa forma, conseguimos evitar
cálculos desnecessários, construir estruturas de dados infinitas e também
melhorar a performance de um encadeamento de operações, pois é possível
otimizar a cadeia de operações como um todo, após avaliar, no fim, o que
realmente se pretendia. A biblioteca Lazy.js (http://danieltao.com/lazy.js/)
tem essa implementação.
1.9.3 EditorConfig.org
O EditorConfig.org (http://editorconfig.org) é um projeto open source que
ajuda equipes de desenvolvedores que utilizam diferentes IDEs e editores de
códigos a manter um estilo consistente no projeto, como, por exemplo,
utilizar dois espaços para indentar, não permitir espaços desnecessários,
inserir uma nova linha no fim do arquivo etc.
O EditorConfig contém plugins para diversos editores, como Atom, Emacs,
IntellijIDEA, NetBeans, Notepad++, Sublime Text, Vim, VS Code e
WebStorm.
Basta ter um arquivo .editorconfig na raiz do projeto e cada desenvolvedor
instalar o plugin correspondente para o seu editor ou IDE. Vou dar uma
sugestão de .editorconfig para você utilizar com a sua equipe:
Arquivo .editorconfig
# EditorConfig is awesome: http://EditorConfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
Se houver o arquivo .editorconfig na raiz do projeto, as configurações pessoais
do editor serão sobrescritas e naquele projeto usará as configurações
definidas nesse arquivo.
/**
* loremipsum.js
*
* Faz uma requisição na API `http://loripsum.net/api/`
* e grava um arquivo com o nome e a quantidade
* de parágrafos especificados
*
* William Bruno, Maio de 2015
* William Bruno, Dezembro de 2020 – Atualizado para es6
*/
const http = require('http');
const fs = require('fs');
const fileName = String(process.argv[2] || '').replace(/[^a-z0-9\.]/gi, '');
const quantityOfParagraphs = String(process.argv[3] || '').replace(/[^\d]/g, '');
const USAGE = 'USO: node loremipsum.js {nomeArquivo} {quantidadeParágrafos}';
if (!fileName || !quantityOfParagraphs) {
return console.log(USAGE);
}
http.get('http://loripsum.net/api/' + quantityOfParagraphs, (res) => {
let text = '';
res.on('data', (chunk) => {
text += chunk;
});
res.on('end', () => {
fs.writeFile(fileName, text, () => {
console.log('Arquivo ' + fileName + ' pronto!');
});
});
})
.on('error', (e) => {
console.log('Got error: ' + e.message);
});
Executando o script:
$ node loremipsum.js teste.txt 10
Arquivo teste.txt pronto!
O arquivo teste.txt foi criado com dez parágrafos de Lorem Ipsum.
Usuário de Unix: caso queira executar a nossa ferramenta de linha de
comando como um script bash, sem digitar node, adicione a seguinte linha
antes do comentário que descreve o arquivo, para que esta seja a linha
número 1 do arquivo:
#!/usr/bin/env node
E conceda permissão de execução ao script:
$ chmod +x loremipsum.js
Pronto. Você poderá usar as duas formas:
$ ./loremipsum.js teste3.txt 13
e
$ node loremipsum.js teste4.txt 14
Uma característica muito importante de um programa NodeJS é que, depois
de executar o programa pelo terminal, irá ocorrer algum processamento,
seguido ou não de uma saída no terminal, o terminal será liberado, e a
memória utilizada no processamento será esvaziada.
2.2 Debug
Durante o desenvolvimento com NodeJS, podemos ter dúvidas sobre alguma
variável, objeto, o que retornou em uma requisição ao banco de dados etc.
Para isso, podemos, da mesma forma como no JavaScript client side, utilizar
a função console.log(). Inclusive já utilizei o console.log() em alguns trechos dos
scripts anteriores. Porém, é uma má prática manter console.log() no meio da
aplicação quando formos colocá-la em produção. Por um simples motivo:
tudo o que escrevermos com console.log() no terminal irá para o arquivo de log
da aplicação, por isso não deixaremos debugs aleatórios em um arquivo tão
importante como esse.
Então, utilizaremos o módulo debug (https://github.com/visionmedia/debug).
Ele trabalha dependendo de uma variável de ambiente chamada DEBUG.
Somente se ela existir e tiver algum valor, o módulo irá imprimir os debugs
correspondentes na tela. Assim não precisamos ficar preocupados em retirar
da aplicação as chamadas à função console.log(), pois usaremos apenas debug().
Instale o módulo debug como uma dependência do projeto:
$ npm install debug --save
Ao rodar esse comando, o npm irá alterar o package.json para que essa
dependência fique salva.
"dependencies": {
"debug": "^4.3.1"
}
Importe o módulo com a função require() para dentro do arquivo .js que você
quer utilizar:
var debug = require('debug')('livro_nodejs');
E depois utilize da mesma forma que faria com o console.log():
debug('Hi!');
Crie a variável de ambiente DEBUG (utilizando export para Unix ou set para
Windows). Podemos pedir para que o debug mostre tudo:
$ export DEBUG=*
E, nesse caso, veremos o debug de outros módulos npm, e não apenas os
nossos, algo mais ou menos assim:
express:router use / query +1ms
express:router:layer new / +0ms
Porém, se estivermos interessados apenas no debug da nossa aplicação,
deveremos declarar a variável de ambiente desta outra forma:
$ export DEBUG=livro_nodejs:*
pois esse foi o namespace que declaramos no momento do require:
let debug = require('debug')('livro_nodejs');
que poderia ser também:
require('debug')('livro_nodejs:model'), require('debug')('livro_nodejs:router')
ou
require('debug')('livro_nodejs:controller')
Depende de como queremos organizar. Uma outra feature bem legal é que ele
mostra o tempo decorrido entre duas chamadas do método debug, algo bem
útil para medir performance. Igualzinho ao que faríamos com o console.time() e
o console.timeEnd().
Note que, quando instalamos o módulo debug com o comando npm install --
save, uma pasta chamada node_modules foi criada no mesmo nível de diretório
do arquivo package.json. Nessa pasta ficarão todas as dependências locais do
projeto.
Não edite os arquivos dessa pasta, por isso eu a adicionei para ser ignorada
no meu arquivo de preferências do Sublime Text e do VS Code. Assim,
quando realizarmos alguma busca ou troca pelo projeto, os arquivos da
node_modules permanecerão intocados.
Além disso, o arquivo package-lock.json foi criado.
2.3 Brincando com TCP
O TCP ou Protocolo de Controle de Transmissão é um dos protocolos de
comunicação de rede de computadores.
Uma característica muito importante de uma ferramenta de linha de comando
é que, após ter sido executada, o processo devolve o cursor do terminal ao
usuário, para que ele possa continuar trabalhando, digitando outros comandos
ou invocando novamente a ferramenta.
No caso de servidores, não acontece isso. O processo não pode simplesmente
fechar. Ele precisa continuar aberto, aguardando conexões, para poder
responder o que for solicitado.
Caso você não tenha um cliente TCP instalado na sua máquina e estiver
usando OS X, você pode baixar via brew (https://brew.sh/index_pt-br):
$ brew install telnet
O comando telnet não vem habilitado por padrão no Windows; para isso,
digite o comando optionalfeatures no Executar do Windows, marque a opção
Cliente Telnet e clique em Ok.
E então conecte via telnet para assistir ao filme Episódio IV:
$ telnet towel.blinkenlights.nl
Assim, uma série de desenhos ASCII será mostrada no terminal, como
demonstra a Figura 2.1.
2.5 Nodemon
Toda vez que alteramos alguma linha de código do nosso programa,
precisamos reiniciar o processo do servidor para que as nossas alterações
sejam refletidas. Para não ficar sempre parando o servidor com Ctrl + C e
iniciando novamente com node <nome do programa> cada vez que alterarmos
um arquivo, existe o módulo Nodemon (https://nodemon.io).
Ele fica ouvindo as alterações dos arquivos no diretório do nosso projeto e,
assim que um arquivo .js, .mjs ou .json do nosso projeto for alterado, o
Nodemon reiniciará o processo NodeJS, agilizando bastante o
desenvolvimento. Instale globalmente:
$ npm install -g nodemon
Agora, em vez de iniciar o servidor com o comando node server-http.js, vamos
usar:
$ nodemon server-http.js
[nodemon] 2.0.6
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node server-http.js`
Server running at http://127.0.0.1:1337/
As configurações do Nodemon podem ser externalizadas num arquivo
chamado nodemon.json na raiz do projeto. Dessa forma customizamos quais
diretórios podem ser ignorados em caso de alterações e quais extensões
queremos que causem o restart do nosso programa enquanto desenvolvemos.
Arquivo nodemon.json
{
"restartable": "rs",
"ignore": [
".git", "node_modules/*"
],
"verbose": true,
"env": { "NODE_ENV": "development" },
"ext": "js mjs json html"
}
Retornando ao arquivo server-http.js, podemos agora incluir novas rotas:
const routes = new Map()
//...
routes.set('/chewbacca', (request, response) =>
response.end('RRRAARRWHHGWWR!\n'))
Ao salvar o arquivo no editor, o Nodemon vai perceber essa alteração e
restartar o processo:
[nodemon] restarting due to changes...
[nodemon] server-http.js
[nodemon] starting `node server-http.js`
Dessa forma não perdemos mais tempo, indo até o terminal, apertando Ctrl +
C e depois iniciando novamente o programa. Podemos, por exemplo, fazer o
endpoint /chewbacca retornar frases aleatórias cada vez que for invocado:
const phrases = [
'RRRAARRWHHGWWR',
'RWGWGWARAHHHHWWRGGWRWRW',
'WWWRRRRRRGWWWRRRR'
]
routes.set('/chewbacca', (request, response) => {
const randomIndex = Math.ceil(Math.random() * phrases.length) -1
const say = phrases[randomIndex]
response.end(`${say}\n`)
})
O Nodemon reiniciará novamente o processo automaticamente, e podemos
nos preocupar somente em testar:
$ curl 'http://localhost:1337/chewbacca'
RRRAARRWHHGWWR
$ curl 'http://localhost:1337/chewbacca'
RRRAARRWHHGWWR
$ curl 'http://localhost:1337/chewbacca'
WWWRRRRRRGWWWRRRR
$ curl 'http://localhost:1337/chewbacca'
RWGWGWARAHHHHWWRGGWRWRW
Arquivo package.json
{
"name": "express",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
"morgan": "~1.9.1"
}
}
Vamos instalar o Nodemon como dependência de desenvolvimento e instalar
as outras dependências que o express-generator declarou:
$ npm i --save-dev nodemon
$ npm i
Podemos, então, encapsular a complexidade de iniciar a aplicação para
desenvolvimento local no package.json.
"scripts": {
"dev": "nodemon ./bin/www",
"start": "node ./bin/www"
},
Basta iniciar com o comando npm run dev ou yarn dev, quando estivermos na
máquina local. Não se preocupe com o código criado, vamos ver com
detalhes o que significa cada parte.
A partir daí, você pode desenvolver o restante da aplicação, trocar o template
engine, criar outras rotas com base nas rotas de exemplo que o comando
express criou etc. A estrutura que o express-generator cria é bem parecida com
a que faremos no Capítulo 5; por isso, pode ser uma boa ideia iniciar um
projeto com esse empurrãozinho na criação dos diretórios e na instalação de
algumas dependências básicas.
{"count":82,"next":"http://swapi.dev/api/people/?page=2","previous":null,"results":
[{"name":"Luke Skywalker","…
Vemos que ela possui uma paginação de dez em dez, e no total 82 pessoas
cadastradas.
Para não usar o módulo http diretamente, instale o Axios
(https://github.com/axios/axios):
$ npm init -y
$ npm i --save axios
O Axios abstrai a interface de uso do modulo http, deixando nosso código
mais expressivo e conciso.
O programa mais simples para a requisição fica:
const axios = require('axios')
axios.get('https://swapi.dev/api/people/')
.then(result => {
console.log(result.data)
})
.then(result => {
process.exit()
})
Testando nosso programa:
$ node index.js
{
count: 82,
next: 'http://swapi.dev/api/people/?page=2',
previous: null,
results: [
{
name: 'Luke Skywalker',
height: '172',
…
Queremos gerar o markdown a seguir:
# Star Wars API
Tem 82 pessoas
Name | Height | Mass | Hair Color | Skin Color | Eye Color | Birth Year | Gender
---------------|--------|------|------------|------------|-----------|------------|-------------
Luke Skywalker | 172 | 77 | Blond | Fair | Blue | 19BBY | male
Visualizando no navegador o preview do código markdown utilizando o site
Stack Edit (https://stackedit.io/app#), será mostrado conforme a Figura 2.2.
Name | Height | Mass | Hair Color | Skin Color | Eye Color | Birth Year |
Gender |
---------------|--------|------|------------|------------|-----------|------------|--------|
${items.map(item => {
return [
item.name,
item.height,
item.mass,
item.hair_color,
item.skin_color,
item.eye_color,
item.birth_year,
item.gender,
''
].join('|')
}).join('\n')}
`
console.log(marked(markdown))
return marked(markdown)
}
axios.get('https://swapi.dev/api/people/')
.then(render)
.then(_ => process.exit())
O resultado é:
$ node index.js
<h1 id="star-wars-api">Star Wars API</h1>
<p>Tem 82 pessoas</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Height</th>
<th>Mass</th>
<th>Hair Color</th>
<th>Skin Color</th>
<th>Eye Color</th>
<th>Birth Year</th>
<th>Gender</th>
</tr>
</thead>
<tbody><tr>
<td>Luke Skywalker</td>
<td>172</td>
…
Com essa etapa pronta, vamos nos concentrar em realizar a paginação para
ler todos os dados. Como não queremos causar impactos na Star Wars API,
vamos fazer uma requisição de cada vez, para isso usaremos generators.
async function* paginate() {
let page = 1
let result;
while (!result || result.status === 200) {
try {
result = await axios.get(`https://swapi.dev/api/people/?page=${page}`)
page++
yield result
} catch (e) {
return e
}
}
}
const getData = async () => {
let results = []
for await (const response of paginate()) {
results = results.concat(response.data.results)
}
return {
count: results.length,
results
}
}
A função paginate() faz requisições de página em página enquanto o status
code retornado for 200. Cada requisição devolve dez pessoas, e são 82 no
total, logo temos nove páginas; ao tentar fazer um request para a décima
página, recebemos um 404 de retorno e, nesse momento, sabemos que
acabamos de recuperar todas as pessoas.
A função getData() usa o for await para aguardar um retorno por vez, concatena
os dados em um array e envia para a função render todas as 82 pessoas.
getData()
.then(render)
.then(result => fs.writeFile('people.html', result))
.then(_ => process.exit())
Ao executar, teremos um arquivo people.html com todas as pessoas da API e o
layout que vemos na Figura 2.3.
Query
Devemos utilizar a query para filtrar dados. Imagine que você tenha uma
URL que, quando acessada, retorna muitos livros. Se quisermos apenas os
livros escritos em português, utilizaremos a query para filtrar esses dados:
/livros?lingua=pt-br
Podemos continuar filtrando e pedir apenas os dez primeiros:
/livros?lingua=pt-br&limite=10
A sintaxe de uma query string é <busca>=<valor>. Indicamos que vamos
concatenar mais uma busca após outra com o caractere & (e comercial). O
início da query string é indicado pelo caractere ? (interrogação), ficando
então uma query string com três parâmetros, assim:
?<query>=<value>&<query2>=<value2>&<query3>=<value3>
Recurso (URI)
É a primeira parte da URL logo após o domínio. Aquela parte que fica entre
barras. Quando construímos uma API, pensamos nos recursos que iremos
disponibilizar e escrevemos as URLs de uma maneira clara e legível para
que a nossa URI identifique claramente o que será retornado.
http://site.com/kamino.jpg, http://site.com/worlds etc.
Parâmetros
Um parâmetro é uma informação variável em uma URI. Aquela parte após
o domínio e o recurso que aceita diferentes valores e, consequentemente,
retorna dados diferentes. Geralmente utilizamos os parâmetros para
informar ids do banco de dados, assim pedimos para esse endpoint apenas
um produto específico.
/worlds/55061dc648ccdc491c6b2b61
Nesse caso, a string 55061dc... é o parâmetro, e worlds é o recurso.
Cabeçalho
São informações adicionais, enviadas na requisição. Se quisermos avisar o
servidor que estamos enviando uma requisição com um conteúdo formatado
em JSON, informamos via cabeçalho.
H "Content-Type: application/json"
Os cabeçalhos não aparecem na URL, e não conseguimos manipulá-los
com HTML, por isso talvez seja difícil identificar exatamente onde eles
estão. Cabeçalhos personalizados eram geralmente prefixados com a letra
X-. Como X-Auth-Token, X-CSRFToken, X-HTTP-Method-Override etc. Porém,
essa convenção caiu em desuso (http://tools.ietf.org/html/rfc6648), e hoje é
encorajado que utilizemos diretamente o nome que queremos, sem prefixo
algum.
Método
É o tipo de requisição que estamos fazendo. Pense no método como um
verbo, ou seja, uma ação. Para cada tipo de ação existe um verbo
correspondente. Os verbos HTTP permitem que uma mesma URL tenha
ações diferentes sob um mesmo recurso, veja:
• GET /troopers/id – retorna um soldado específico pelo seu id.
• PUT /troopers/id – atualiza um soldado pelo seu id. No PUT toda a
entidade deve ser enviada.
• PATCH /troopers/id – atualiza alguma informação do soldado de tal id.
Diferente do PUT, o PATCH não requer que todas as informações sejam
enviadas, mas apenas aquelas que forem de fato modificadas.
• DELETE /troopers/id – exclui o soldado de id 7.
Ou então:
• GET /troopers – retorna todos os soldados.
• POST /troopers – cria um novo soldado.
Nossa aplicação neste livro irá utilizar quatro métodos HTTP: POST, GET,
PUT e DELETE. O verbo PATCH é perfeito para campos edit in place,1 por
exemplo.
Os métodos GET, HEAD e PUT são idempotentes, ou seja, o resultado de
uma requisição realizada com sucesso é independente do número de vezes
que é executada.
Porém, o HTML só implementa dois verbos: GET e POST. Para que
consigamos utilizar os demais, precisamos de alguns truques, como enviar
na query string do action do formulário o método que queremos utilizar. O
servidor que receber a requisição deverá entender isso.
<form action="/planets?_method=DELETE" method="POST">
Dado
É o corpo da requisição, ou seja, os dados que queremos enviar. Pode ser
texto puro, formatado em XML, em JSON, imagem ou qualquer outro tipo
de mídia. Em nosso caso, será uma string formatada em JSON contendo
informações do usuário.
Status code
O status code (http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) é
uma representação numérica da resposta, um inteiro de três dígitos que
informa o estado do retorno. Existem dois álbuns na internet que ilustram
com gatos (https://http.cat/) ou cachorros (http://httpstatusdogs.com) os
possíveis status codes. Nós os classificamos em cinco tipos, de acordo com
o número da centena:
• 1xx – indica uma resposta provisória.
• 2xx – indica que a requisição foi recebida, entendida e aceita.
• 3xx – indica que futuras ações precisam ser feitas para que a requisição
seja completada.
• 4xx – indica algum erro do cliente.
• 5xx – indica algum erro no servidor, como, por exemplo, que ele não foi
capaz de processar a requisição.
Alguns status codes bem comuns são:
• 200 para indicar okay, tudo certo. Responderemos com 200 qualquer ação
que tenha ocorrido bem, seja uma listagem, atualização ou exclusão.
• 201 para quando algo for criado. O POST para criar um novo usuário será
o único que não responderemos com 200; apesar de também ser
tecnicamente correto utilizar 200 para indicar que deu certo, utilizaremos
201.
• 204 para indicar que não há retorno, comumente usado após um
DELETE.
• 301 para redirecionamentos permanentes, quando algum recurso for
movido de lugar por tempo indeterminado ou para sempre.
• 302 para redirecionamentos temporários.
• 304 para indicar que algo não foi modificado e pode ser usado o conteúdo
do cache, por exemplo.
• 401 para indicar um acesso não autorizado.
• 403 para indicar uma ação proibida.
• 404 para indicar que o recurso solicitado não existe.
• 409 para indicar um conflito, como uma criação duplicada.
• 422 para indicar que há algum erro no pedido do cliente.
• 500 para indicar um erro genérico interno no servidor.
Dado
É o corpo da resposta, o resultado da requisição. Pode ser uma imagem, um
vídeo, um texto etc. Dependendo da requisição que estamos fazendo, essa é
a parte mais importante da resposta.
Cabeçalho
Assim como o cabeçalho da requisição, o cabeçalho da resposta traz
informações adicionais: se o conteúdo foi devolvido com algum tipo de
compressão (gzip), informações sobre qual tecnologia do servidor
respondeu à solicitação, o tamanho do conteúdo respondido, informações
sobre o cache etc.
Cookies
Fazem parte da resposta, são arquivos temporários, gravados no navegador,
com escopo do site que os criou, para gravar e manipular informações.
Client-server
A arquitetura e a responsabilidade do cliente e do servidor são bem
definidas. O cliente não se preocupa com comunicação com banco de
dados, gerenciamento de cache, log etc., enquanto o servidor não se
preocupa com interface, experiência do usuário etc., permitindo, assim, a
evolução independente das duas arquiteturas.
Stateless
Cada requisição de um cliente ao servidor é independente da anterior. Toda
requisição deve conter todas as informações necessárias para que o servidor
consiga respondê-la corretamente.
Cacheable
Uma camada de cache deve ser implementada para evitar processamento
desnecessário, pois vários clientes podem solicitar o mesmo recurso num
curto espaço de tempo.
Uniform interface
O contrato da comunicação deve seguir algumas regras para facilitar a
comunicação entre cliente e servidor:
• escritos em letra minúscula;
• separados com hífen quando necessário;
• recursos descritos no plural;
• descritivos e concisos;
• representação clara do recurso;
• resposta autoexplicativa;
• hypermedia;
• utilizar o verbo HTTP mais adequado;
• retornar o status code correspondente à ação realizada.
Layered system
A aplicação deve ser composta de camadas, e estas devem ser fáceis de se
alterar, tanto para adicionar novas camadas quanto para removê-las. Um
dos princípios dessa restrição é que a aplicação deve ficar atrás de um
intermediador, como um load balancer; dessa forma, o servidor da
aplicação se comunica com o load balancer, e o cliente requisita a ele, sem
conhecer necessariamente os servidores de backend.
Code-on-demand
Permite que diferentes clientes se comportem de maneiras específicas,
mesmo utilizando exatamente os mesmos serviços providos pelo servidor.
POST (create)
Cadastra um novo registro.
$ curl -H "Content-Type: application/json" \
-d '{"name":"Death Star"}' http://127.0.0.1:3000/weapons
GET (retrieve)
Retorna alguma informação do servidor, seja uma lista ou um único item.
$ curl -H "Content-Type: application/json" \
http://127.0.0.1:3000/quotes
$ curl -H "Content-Type: application/json" \
http://127.0.0.1:3000/quotes/55060ceba8cf25db09f3b216
DELETE (delete)
Remove um item ou um dado.
$ curl -X POST -H "Content-Type: application/json" \
-H "X-HTTP-Method-Override: DELETE" \
http://127.0.0.1:3000/clones/55061dc648ccdc491c6b2b61
O termo CRUD provém destas quatro ações: Create, Retrieve, Update e
Delete.
1 edit in place: interação na qual uma parte do texto se transforma em um campo de entrada
de dados; o usuário é capaz de enviar para o servidor apenas um campo de cada vez.
4
CAPÍTULO
Bancos de dados
4.1.1 Modelagem
Modelando a nossa entidade stormtrooper para o modelo relacional, seguindo
as Formas Normais, teremos a seguinte estrutura, representada na Figura 4.1.
Figura 4.1 – Estrutura das tabelas no Postgres.
Criaremos todas as tabelas a seguir:
livro_nodejs=# CREATE TABLE patents (
id serial PRIMARY KEY,
name TEXT UNIQUE NOT NULL
);
CREATE TABLE divisions (
id serial PRIMARY KEY,
name TEXT UNIQUE NOT NULL
);
CREATE TABLE stormtroopers (
id serial PRIMARY KEY,
name TEXT NOT NULL,
nickname TEXT NOT NULL,
id_patent INT NOT NULL,
FOREIGN KEY (id_patent) REFERENCES patents(id)
);
CREATE TABLE stormtrooper_division (
id_stormtrooper INT NOT NULL,
id_division INT NOT NULL,
FOREIGN KEY (id_stormtrooper) REFERENCES stormtroopers(id),
FOREIGN KEY (id_division) REFERENCES divisions(id)
);
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
A tabela stormtroopers tem os atributos id, name, nickname e id_patent.
livro_nodejs=# \d stormtroopers
Table "public.stormtroopers"
Column | Type | Collation | Nullable | Default
-----------+---------+-----------+----------+---------------------------------------
id | integer | | not null | nextval('stormtroopers_id_seq'::regclass)
name | text | | not null |
nickname | text | | not null |
id_patent | integer | | not null |
Indexes:
"stormtroopers_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"stormtroopers_id_patent_fkey" FOREIGN KEY (id_patent) REFERENCES
patents(id)
Referenced by:
TABLE "stormtrooper_division" CONSTRAINT
"stormtrooper_division_id_stormtrooper_fkey" FOREIGN KEY (id_stormtrooper)
REFERENCES stormtroopers(id)
Quanto às outras propriedades – patente e divisão –, seguindo as regras de
normalização, não podemos cadastrar nessa mesma tabela, pois aí teríamos
uma duplicação de informações e atributos multivalorados. O correto é criar
mais duas tabelas: patents e divisions.
Relacionamento 1:N
Cada soldado tem uma patente, então temos um relacionamento 1 para n. Por
isso, criamos a tabela patents.
livro_nodejs=# \d patents
Table "public.patents"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+-------------------------------------
id | integer | | not null | nextval('patents_id_seq'::regclass)
name | text | | not null |
Indexes:
"patents_pkey" PRIMARY KEY, btree (id)
"patents_name_key" UNIQUE CONSTRAINT, btree (name)
Referenced by:
TABLE "stormtroopers" CONSTRAINT "stormtroopers_id_patent_fkey" FOREIGN
KEY (id_patent) REFERENCES patents(id)
Agora cadastramos as possíveis patentes:
livro_nodejs=# INSERT INTO patents (name) VALUES ('Soldier'), ('Commander'),
('Captain'), ('Lieutenant'), ('Sergeant');
INSERT 0 5
Isso pronto, podemos inserir o clone. Ops! Soldado CC-1010, que tem o
apelido Fox e contém a patente de Comandante, que é o id 2 da tabela patents.
livro_nodejs=# INSERT INTO stormtroopers (name, nickname, id_patent) VALUES
('CC-1010', 'Fox', 2);
INSERT 0 1
Para visualizar essa informação, utilizamos um INNER JOIN:
livro_nodejs=# SELECT stormtroopers.id, stormtroopers.name, nickname, patents.name
FROM stormtroopers INNER JOIN patents ON patents.id = stormtroopers.id_patent;
id | name | nickname | name
----+---------+----------+-----------
1 | CC-1010 | Fox | Commander
(1 row)
Relacionamento N:N
Um soldado pode pertencer a mais de uma divisão, por isso precisamos de
um relacionamento n para n, em que cada soldado tem n divisões e cada
divisão tem n soldados. Usaremos a tabela divisions.
livro_nodejs=# \d divisions
Table "public.divisions"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------------------------------------
id | integer | | not null | nextval('divisions_id_seq'::regclass)
name | text | | not null |
Indexes:
"divisions_pkey" PRIMARY KEY, btree (id)
"divisions_name_key" UNIQUE CONSTRAINT, btree (name)
Referenced by:
TABLE "stormtrooper_division" CONSTRAINT
"stormtrooper_division_id_division_fkey" FOREIGN KEY (id_division) REFERENCES
divisions(id)
Então inserimos as divisões:
livro_nodejs=# INSERT INTO divisions (name) VALUES ('Breakout Squad'), ('501st
Legion'), ('35th Infantry'), ('212th Attack Battalion'), ('Squad Seven'), ('44th Special
Operations Division'), ('Lightning Squadron'), ('Coruscant Guard');
INSERT 0 8
Por isso é necessária uma tabela de relacionamento para fazer o n:n, chamada
stormtrooper_division.
livro_nodejs=# \d stormtrooper_division
Table "public.stormtrooper_division"
Column | Type | Collation | Nullable | Default
-----------------+---------+-----------+----------+---------\
id_stormtrooper | integer | | not null |
id_division | integer | | not null |
Foreign-key constraints:
"stormtrooper_division_id_division_fkey" FOREIGN KEY (id_division)
REFERENCES divisions(id)
"stormtrooper_division_id_stormtrooper_fkey" FOREIGN KEY (id_stormtrooper)
REFERENCES stormtroopers(id)
Para inserir a divisão do Comandante Fox (id 1 da tabela stormtroopers),
precisamos de dois inserts na tabela de relacionamento, pois ele passou por
dois postos: 501st Legion (id 2 da tabela divisions) e Coruscant Guard (id 8 da
tabela divisions).
livro_nodejs=# INSERT INTO stormtrooper_division (id_stormtrooper, id_division)
VALUES (1, 2), (1, 8);
INSERT 0 2
Podemos ver como fica esse cadastro utilizando um JOIN:
livro_nodejs=# SELECT id_stormtrooper, name, nickname, id_patent,
stormtrooper_division.id_division
FROM stormtroopers
INNER JOIN stormtrooper_division ON stormtroopers.id =
stormtrooper_division.id_stormtrooper;
id_stormtrooper | name | nickname | id_patent | id_division
-----------------+------------+----------+-----------+-------------
1 | CC-1010 | Fox | 2| 2
1 | CC-1010 | Fox | 2| 8
Se quisermos saber o nome da patente e o nome da divisão, em vez do id
delas, precisamos de mais dois joins, um na tabela patents e outro na tabela
divisions.
livro_nodejs=# SELECT id_stormtrooper, stormtroopers.name, nickname, patents.name,
divisions.name
FROM stormtroopers
INNER JOIN stormtrooper_division ON stormtroopers.id =
stormtrooper_division.id_stormtrooper
INNER JOIN patents ON patents.id = stormtroopers.id_patent
INNER JOIN divisions ON divisions.id = stormtrooper_division.id_division;
id_stormtrooper | name | nickname | name | name
-----------------+------------+----------+-----------+------------------------
1 | CC-1010 | Fox | Commander | 501st Legion
1 | CC-1010 | Fox | Commander | Coruscant Guard
O Comandante Fox parece duplicado, pois é assim que os bancos SQL tratam
os relacionamentos muitos para muitos (n:n). Caberá à aplicação saber
trabalhar com essas informações agora.
Podemos cadastrar mais alguns soldados e inserir a relação deles com as
divisões:
livro_nodejs=# INSERT INTO stormtroopers (name, nickname, id_patent) VALUES
('CT-7567', 'Rex', 3), ('CC-2224', 'Cody', 2), ('', 'Hardcase', 1), ('CT-27-5555', 'Fives', 1);
INSERT 0 4
livro_nodejs=# INSERT INTO stormtrooper_division (id_stormtrooper, id_division)
VALUES (5, 2), (4, 2), (3, 4), (2, 2);
INSERT 0 4
Feito isso, com a mesma query anterior, conseguimos recuperar as
informações de todos eles:
livro_nodejs=# SELECT
stormtroopers.id,
stormtroopers.name,
nickname,
patents.name AS patent,
divisions.name AS division
FROM stormtroopers
LEFT JOIN stormtrooper_division ON stormtroopers.id =
stormtrooper_division.id_stormtrooper
LEFT JOIN patents ON patents.id = stormtroopers.id_patent
LEFT JOIN divisions ON divisions.id = stormtrooper_division.id_division;
Na Figura 4.2 vemos como fica representado no terminal:
4.1.2 node-postgres
Usando o módulo pg com NodeJS, fica assim:
$ npm i pg
Arquivo pg-create.js
const { Client } = require('pg')
const client = new Client({
user: 'wbruno',
password: '',
host: 'localhost',
port: 5432,
database: 'livro_nodejs',
})
client.connect()
const params = ['CT-5555', 'Fives', 2]
const sql = `INSERT INTO stormtroopers (name, nickname, id_patent)
VALUES ($1::text, $2::text, $3::int)`
client.query(sql, params)
.then(result => {
console.log(result)
process.exit()
})
E o script para o SELECT:
Arquivo pg-retrieve.js
const { Client } = require('pg')
const client = new Client({
user: 'wbruno',
password: '',
host: 'localhost',
port: 5432,
database: 'livro_nodejs',
})
client.connect()
const params = ['CT-5555']
const sql = `SELECT * FROM stormtroopers WHERE name = $1::text`
client.query(sql, params)
.then(result => {
console.log(result.rows)
process.exit()
})
Executando:
$ node pg-retrieve.js
[
{ id: 6, name: 'CT-5555', nickname: 'Fives', id_patent: 2 }
]
4.2 MongoDB
O MongoDB não utiliza o conceito de tabelas, schemas, linhas ou SQL. Não
tem chaves estrangeiras, triggers e procedures. E não se propõe a resolver
todos os problemas de armazenamento de dados. Simplesmente aceita o fato
de que talvez não seja o banco de dados ideal para todo mundo.
Ufa! Agora que já o assustei, posso falar sobre as coisas boas do MongoDB.
Os engenheiros do MongoDB escreveram um banco de dados extremamente
rápido e escalável, capaz de suportar uma enorme quantidade de dados. Uma
instalação ideal de MongoDB deve ser composta de, no mínimo, três
instâncias funcionando como replica set (arquitetura master e slave) ou em
shardings (arquitetura em que os dados são divididos em diferentes nós).
Trabalhando com replica set, os dados estão sempre triplicados; em caso de
falha de alguma instância, as restantes realizam uma votação e elegem uma
nova master para continuar respondendo. Assim, quando a máquina voltar,
ela entrará em sincronia com as que ficaram de pé.
Ele trabalha com um conceito de documentos em vez de linhas, e coleções
em vez de tabelas, conforme o comparativo da Figura 4.3.
4.2.1 Modelagem
O MongoDB é um banco de dados open source, orientado a documentos e
NoSQL, ou seja, não relacional. Então a modelagem é bem diferente da que
vimos no subitem anterior. Não pensaremos mais em tabelas e como uma está
relacionada com a outra, mas sim em documentos, e mais na entidade que
queremos representar.
{
"name": "CT-1010",
"nickname": "Fox",
"divisions": [
"501st Legion",
"Coruscant Guard"
],
"patent": "Commander"
}
No MongoDB não há a necessidade de criar o database. Ele será criado
quando for utilizado pela primeira vez. Nem de criar uma collection, que será
criada quando o primeiro registro for inserido. Com o comando use, nós
trocamos de database.
Para entrar no terminal do mongo, após instalar o MongoDB na sua máquina,
crie um diretório chamado /data/db na raiz do seu computador, ou seja:
C:/data/db se for Windows, ou /data/db se for um sistema Unix-like. Deixe uma
janela de terminal aberta com:
$ mongod
e outra para acessar o MongoDB, digite:
$ mongo
> use livro_nodejs;
switched to db livro_nodejs
O console do MongoDB é JavaScript, assim como o console do NodeJS,
logo, também podemos escrever qualquer JavaScript válido, como uma
expressão regular (parafraseando o Aurelio Vargas na regex).
> /^(b{2}|[^b]{2})$/.test('aa');
true
Um projeto muito legal é o Mongo Hacker
(https://github.com/TylerBrock/mongo-hacker), com ele instalado, melhora a
experiência do shell do mongo, ao adicionar comandos e diversos hacks no
arquivo ~/.mongorc.js.
Para listar todos os databases disponíveis, use o comando show dbs.
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
Se você estiver com o Mongo Hacker instalado, irá aparecer na frente de cada
banco de dados o tamanho daquele banco em gigabytes.
Diferentemente da normalização que fizemos no banco de dados relacional,
um banco de dados orientado a documentos incentiva você a duplicar
informações. Então não teremos as tabelas auxiliares de patentes e divisões.
Será tudo parte do documento stormtroopers.
A palavra db é um ponteiro que aponta para o database em que estamos
logados.
> db
livro_nodejs
A sintaxe para realizar alguma coisa pelo console é:
db.<nomecollection>.<operacao>.
Inserindo registros
Podemos apenas inserir o clone diretamente, sem ter criado a collection
previamente:
> db.stormtroopers.insert({ name: 'CT-1010', nickname: 'Fox', divisions: ['501st Legion',
'Coruscant Guard'], patent: 'Commander' });
WriteResult({ "nInserted" : 1 })
Com isso, o banco vai automaticamente criar a collection stormtroopers e
persisti-la no disco para nós:
> show collections;
stormtroopers
system.indexes
A collection system.indexes é o local onde são guardados os índices.
Consultando o soldado que acabamos de inserir, vemos que foi gerado um _id
para esse documento:
> db.stormtroopers.findOne()
{
"_id" : ObjectId("5569be0bf837c934405b15d0"),
"name" : "CT-1010",
"nickname" : "Fox",
"divisions" : [
"501st Legion",
"Coruscant Guard"
],
"patent" : "Commander"
}
Esse _id fica indexado na collection system.indexes. O ObjectId() é uma função
interna do MongoDB que garante que esse _id será único. Dentro do hash de
24 caracteres do ObjectId, existe a informação do segundo em que aquele
registro foi inserido no MongoDB.
> new ObjectId().getTimestamp()
ISODate("2015-12-17T00:00:07Z")
Inserir mais clones é tão simples quanto enviar um array para o banco de
dados; na verdade, podemos de fato criar uma variável com um array e
depois inseri-lo.
> var clones = [{ nickname: 'Hardcase', divisions: ['501st Legion'], patent: 'Soldier' }, {
name: 'CT-27-5555', nickname: 'Fives', divisions: ['Coruscant Guard'], patent: 'Soldier' },
{ name: 'CT-2224', nickname: 'Cody', divisions: ['212th Attack Battalion'], patent:
'Commander' },
{ name: 'CT-7567', nickname: 'Rex', divisions: ['501st Legion'],
patent: 'Capitain' }];
> db.stormtroopers.insert(clones);
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 4,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
})
Selecionando resultados
Utilizando o comando find(), consigo fazer uma query e trazer todo mundo:
> db.stormtroopers.find()
{ "_id" : ObjectId("5569bf08f837c934405b15d1"), "name" : "CT-1010", "nickname" :
"Fox", "divisions" : [ "501st Legion", "Coruscant Guard" ], "patent" : "Commander" }
{ "_id" : ObjectId("5569bf24f837c934405b15d2"), "nickname" : "Hardcase", "divisions"
: [ "501st Legion" ], "patent" : "Soldier" }
{ "_id" : ObjectId("5569bf24f837c934405b15d3"), "name" : "CT-27-5555", "nickname"
: "Fives", "divisions" : [ "Coruscant Guard" ], "patent" : "Soldier" }
{ "_id" : ObjectId("5569bf24f837c934405b15d4"), "name" : "CT-2224", "nickname" :
"Cody", "divisions" : [ "212th Attack Battalion" ], "patent" : "Commander" }
{ "_id" : ObjectId("5569bf24f837c934405b15d5"), "name" : "CT-7567", "nickname" :
"Rex", "divisions" : [ "501st Legion" ], "patent" : "Capitain" }
Se quiséssemos que o banco não retornasse o atributo _id, bastaria passar id: 0
como segundo argumento da função find(). O primeiro é a query, e o segundo,
quais campos queremos ou não retornar.
> db.stormtroopers.find({}, { _id: 0 })
{ "name" : "CT-1010", "nickname" : "Fox", "divisions" : [ "501st Legion", "Coruscant
Guard" ], "patent" : "Commander" }
{ "nickname" : "Hardcase", "divisions" : [ "501st Legion" ], "patent" : "Soldier" }
{ "name" : "CT-27-5555", "nickname" : "Fives", "divisions" : [ "Coruscant Guard" ],
"patent" : "Soldier" }
{ "name" : "CT-2224", "nickname" : "Cody", "divisions" : [ "212th Attack Battalion" ],
"patent" : "Commander" }
{ "name" : "CT-7567", "nickname" : "Rex", "divisions" : [ "501st Legion" ], "patent" :
"Capitain" }
Caso quiséssemos retornar apenas alguns campos, passaríamos esse campo
com o número 1, indicando um true:
> db.stormtroopers.find({}, { _id: 0, nickname: 1, divisions: 1 })
{ "nickname" : "Fox", "divisions" : [ "501st Legion", "Coruscant Guard" ] }
{ "nickname" : "Hardcase", "divisions" : [ "501st Legion" ] }
{ "nickname" : "Fives", "divisions" : [ "Coruscant Guard" ] }
{ "nickname" : "Cody", "divisions" : [ "212th Attack Battalion" ] }
{ "nickname" : "Rex", "divisions" : [ "501st Legion" ] }
Realizando buscas
Podemos realizar buscas por qualquer um dos atributos do nosso documento,
como, por exemplo, contar quantos são os comandantes:
> db.stormtroopers.find({ patent: 'Commander' }).count()
2
ou se quisermos saber quantos clones pertencem à 501st Legion:
> db.stormtroopers.find({ divisions: { $in: ['501st Legion'] } }).count()
3
Utilizar .find().count() é uma forma lenta de saber quantos registros existem,
pois primeiro subimos os registros para a memória com o .find() e depois
perguntamos quantos são. Podemos usar o count diretamente, com qualquer
query que quisermos:
> db.stormtroopers.count({ divisions: { $in: ['501st Legion'] } })
3
Podemos utilizar expressões regulares para simular um LIKE do SQL e buscar
um clone por parte do seu nome. Com a expressão /CT-2(.*)/, teremos como
retorno todos os clones que tenham o nome iniciado em CT-2:
> db.stormtroopers.find({ name: /CT-2(.*)/ })
{ "_id" : ObjectId("5569c80b17fa3690d24de04b"), "name" : "CT-27-5555", "nickname"
: "Fives", "divisions" : [ "Coruscant Guard" ], "patent" : "Soldier" }
{ "_id" : ObjectId("5569c80b17fa3690d24de04c"), "name" : "CT-2224", "nickname" :
"Cody", "divisions" : [ "212th Attack Battalion" ], "patent" : "Commander" }
Para encontrar todos os nomes que terminam com o número 5 – { name: /5$/ }
– ou todos que começam com a letra F – { nickname: /^F/ }.
O método .distinct() pode ser usado para se ter uma ideia dos valores únicos do
database.
> db.stormtroopers.distinct('patent')
[ "Commander", "Soldier", "Capitain" ]
E funciona também com arrays:
> db.stormtroopers.distinct('divisions')
[
"501st Legion",
"Coruscant Guard",
"212th Attack Battalion",
"Grand Army of the Republic"
]
Atualizando informações
Com o comando update(), nós podemos atualizar um ou vários registros. Para
trocar o nome do soldado Fives de CT-27-5555 para CT-5555, procurando
pelo apelido, fazemos assim:
> db.stormtroopers.update({ nickname: 'Fives' }, { $set: { name: 'CT-5555' } });
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.stormtroopers.find({ nickname: 'Fives' })
{ "_id" : ObjectId("5569c127f837c934405b15d7"), "name" : "CT-5555", "nickname" :
"Fives", "divisions" : [ "Coruscant Guard" ], "patent" : "Soldier" }
Note que utilizei o operador $set para que o MongoDB entendesse que quero
atualizar um dos campos desse documento, e não ele todo. Senão, ele iria
apagar todos os outros e apenas manter o que eu enviei.
Por padrão, o .update() não realiza múltiplas operações, o que quer dizer que,
caso a query case mais de um registro, apenas o primeiro encontrado é que
será atualizado, é como se fosse uma “proteçãozinha” contra um UPDATE
sem WHERE. Porém, se soubermos exatamente o que estamos fazendo,
poderemos usar o terceiro parâmetro para dizer que queremos sim realizar um
update em vários documentos.
db.stormtroopers.update({}, { $set: { age: 32 } }, { multi: 1 });
Excluindo registros
A sintaxe do comando remove() é bem semelhante ao find(), por aceitar um
argumento que fará uma busca nos registros. Informamos a query como
primeiro argumento, e o remove() irá apagar todos os registros que atenderem
a essa busca do banco de dados. Então, para excluir o Rex pelo apelido,
basta:
> db.stormtroopers.remove({ nickname: 'Rex' })
WriteResult({ "nRemoved" : 1 })
Uma diferença muito importante do MongoDB para o Postgres, que você
deve ter notado, é que nós inserimos todas as informações diretamente no
documento, em vez de criarmos tabelas auxiliares.
A modelagem em bancos de dados NoSQL incentiva esse tipo de duplicação
de dados, já que não perdemos o nosso poder de realizar consultas. No
entanto, se tivéssemos desnormalizado o atributo divisions no SQL, não
conseguiríamos realizar pesquisas nele, ou a performance seria bem ruim, por
isso separamos em diversas tabelas.
Outra forma de apagar registros é utilizar o método .drop(). A diferença é que
o remove() mantém os índices e as constrains (índice único) e pode ser
aplicado a um documento, alguns ou todos, enquanto o drop limpa toda a
collection, removendo os registros e os índices.
> db.stormtroopers.drop()
true
Export/Backup
Ao instalar o MongoDB, outros dois pares de executáveis também ficam
disponíveis, são eles: mongoexport/mongoimport e mongodump/mongorestore. A
forma de uso é muito simples, podemos salvar os dados em arquivos,
utilizando o mongoexport:
$ mongoexport -d livro_nodejs -c stormtroopers > mongodb.json
2021-01-04T09:45:43.052-0300 connected to: mongodb://localhost/
2021-01-04T09:45:43.056-0300 exported 5 records
E podemos usar o mongoimport para importar esse arquivo:
$ mongoimport -d livro_nodejs -c stormtroopers --drop < mongodb.json
2021-01-04T09:53:09.584-0300 connected to: mongodb://localhost/
2021-01-04T09:53:09.584-0300 dropping: livro_nodejs.stormtroopers
2021-01-04T09:53:09.765-0300 5 document(s) imported successfully. 0 document(s)
failed to import.
Utilizo a flag --drop para limpar a collection do servidor e aceitar apenas os
dados do arquivo. Caso queira fazer uma importação incremental, não use a
flag --drop.
O arquivo dessa exportação está disponível em:
https://github.com/wbruno/livro-nodejs/blob/main/resources/mongodb.json.
Quando exportamos uma collection, apenas os dados são salvos, ao contrário
do mongodump, que exporta também a estrutura, ou seja, os índices.
4.2.2 mongoist
Usando o módulo mongoist (https://github.com/mongoist/mongoist) com
NodeJS:
$ npm i mongoist
Após instalado, importamos a lib e conectamos no servidor do banco de
dados:
const mongoist = require('mongoist')
const db = mongoist('mongodb://localhost:27017/livro_nodejs')
A sintaxe de conexão é:
mongodb://<usuario>:<senha>@<servidor>:<porta>/<database> ?replicaSet=<nome do
replica set>
Como estamos conectando em localhost, não há usuário, senha nem
replicaSet. Vamos criar um arquivo que insere soldados na base e, para isso,
basta chamar a função .insert(), como fizemos quando estávamos conectados
no mongo shell.
Arquivo mongo-create.js
const mongoist = require('mongoist')
const db = mongoist('mongodb://localhost:27017/livro_nodejs')
const data = {
"name" : "CT-5555",
"nickname" : "Fives",
"divisions" : [ "Coruscant Guard" ],
"patent" : "Soldier"
}
db.stormtroopers.insert(data)
.then(result => {
console.log(result)
process.exit()
})
Note que db.stormtroopers.insert() retorna uma promise, por isso encadeamos um
.then() para poder imprimir o resultado da execução. Invocamos o método
process.exit() para liberar o terminal, avisando que o nosso script encerrou.
$ node mongo-create.js
{
name: 'CT-5555',
nickname: 'Fives',
divisions: [ 'Coruscant Guard' ],
patent: 'Soldier',
_id: 5fee0a86eaa0d28eea176f70
}
Agora, faremos outro arquivo para recuperar o que está gravado no banco.
Arquivo mongo-retrieve.js
const mongoist = require('mongoist')
const db = mongoist('mongodb://localhost:27017/livro_nodejs')
db.stormtroopers.find()
.then(result => {
console.log(result)
process.exit()
})
Note que é bem parecido, mas agora usamos a função .find().
$ node mongo-retrieve.js
[
{
_id: 5fee0a86eaa0d28eea176f70,
name: 'CT-5555',
nickname: 'Fives',
divisions: [ 'Coruscant Guard' ],
patent: 'Soldier'
}
]
O retorno vem dentro de colchetes, pois o método find pode retornar uma lista
documentos, a depender da query.
4.3 Redis
O Redis (https://redis.io/) é um servidor de estrutura de dados. É open source,
em memória, e armazena chaves com durabilidade opcional, usado como
database, cache ou mensageria. Suporta estruturas de dados, como strings,
hashes, listas, sets etc. Assim como o MongoDB, também é um NoSQL e é
orientado a chave-valor.
Os comandos mais básicos do Redis são o set, usado para guardar uma chave
com um valor, e o get para recuperar o valor daquela chave. O comando del
apaga a chave especificada e podemos, inclusive, executar a ordem 66, com o
comando flushall e apagar todas as chaves do storage.
4.3.1 Modelagem
Não fazemos queries complexas no Redis, apenas basicamente retornamos
valores dada uma certa chave exata, então a modelagem dos valores pode ser
qualquer coisa, desde valores simples até objetos.
Após instalar o servidor do Redis, execute o comando (em seu sistema Unix-
like):
$ redis-server
para subir o servidor e
$ redis-cli
127.0.0.1:6379> keys *
(empty list or set)
para se conectar no Redis. O comando keys lista as chaves existentes, ainda
não tenho nenhuma, pois acabei de subir o servidor.
127.0.0.1:6379> set obi-wan 'Não há emoção, há a paz.'
OK
127.0.0.1:6379> get obi-wan
"Não há emoção, há a paz."
Podemos sobrescrever o valor de uma chave apenas setando-a novamente:
127.0.0.1:6379> set jedi-code 'A emocao, ainda a paz. A ignorancia, ainda o
conhecimento. Paixao, ainda serenidade. Caos, ainda a harmonia. Morte, mas a Forca.'
OK
127.0.0.1:6379> set jedi-code 'Nao ha emocao, ha a paz. Nao ha ignorancia, ha
conhecimento. Nao ha paixao, ha serenidade.Nao ha caos, ha harmonia. Nao ha morte, ha
a Forca.'
OK
127.0.0.1:6379> get jedi-code
"Nao ha emocao, ha a paz. Nao ha ignorancia, ha conhecimento. Nao ha paixao, ha
serenidade.Nao ha caos, ha harmonia. Nao ha morte, ha a Forca."
É possível realizar uma busca por chaves:
127.0.0.1:6379> set odan-urr 'Nao ha ignorancia, ha conhecimento.'
OK
127.0.0.1:6379> keys odan*
1) "odan-urr"
Porém, não pelos valores, por isso que não dizemos que o Redis é um banco
de dados.
Outro recurso muito útil é o TTL (Time to Live), em que escolhemos
determinado tempo em que uma chave deve existir, após certo tempo ela
simplesmente desaparece (o Redis se encarrega de apagá-la). Usamos o
comando set e depois o expire para dizer quantos segundos aquela chave deve
permanecer, e depois o comando ttl para ver quanto tempo de vida ainda
resta.
127.0.0.1:6379> set a-ameaca-fantasma 'Episode I'
OK
127.0.0.1:6379> expire a-ameaca-fantasma 327
(integer) 1
127.0.0.1:6379> ttl a-ameaca-fantasma
(integer) 136
127.0.0.1:6379> ttl a-ameaca-fantasma
(integer) 4
127.0.0.1:6379> ttl a-ameaca-fantasma
(integer) -2
Uma vez expirado o tempo daquela chave, o retorno é -2.
127.0.0.1:6379> get a-ameaca-fantasma
(nil)
Com o comando info, é possível ter uma rápida ideia do que está acontecendo
com os recursos do servidor.
127.0.0.1:6379> info memory
# Memory
used_memory:1007808
used_memory_human:984.19K
used_memory_rss:2195456
used_memory_rss_human:2.09M
used_memory_peak:1008656
used_memory_peak_human:985.02K
total_system_memory:8589934592
total_system_memory_human:8.00G
used_memory_lua:37888
used_memory_lua_human:37.00K
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
mem_fragmentation_ratio:2.18
mem_allocator:libc
Para uma boa performance de leitura, é indicado que a máquina na qual o
Redis será instalado tenha memória RAM suficiente para comportar todos os
dados que você pretende armazenar nele.
4.3.2 node-redis
Em nossas aplicações NodeJS, é bem comum utilizar o Redis para cache ou
para guardar a sessão dos usuários. Usando o módulo node-redis
(https://github.com/NodeRedis/node-redis):
$ npm i redis
podemos conectar no servidor do Redis e inserir a nossa chave:
Arquivo redis-create.js
const redis = require('redis');
const { promisify } = require('util');
const client = redis.createClient({
host: 'localhost',
port: 6379
})
const setAsync = promisify(client.set).bind(client);
setAsync('jedi-code', 'Nao ha emocao, ha a paz. Nao ha ignorancia, ha conhecimento.
Nao ha paixao, ha serenidade.Nao ha caos, ha harmonia. Nao ha morte, ha a Forca.')
.then(result => {
console.log(result)
process.exit()
})
Executando:
$ node redis-create.js
OK
E agora, para conferir o que foi escrito no Redis:
Arquivo redis-retrieve.js
const redis = require('redis');
const { promisify } = require('util');
const client = redis.createClient({
host: 'localhost',
port: 6379
})
const getAsync = promisify(client.get).bind(client);
getAsync('jedi-code')
.then(result => {
console.log(result)
process.exit()
})
Executando:
$ node redis-retrieve.js
Nao ha emocao, ha a paz. Nao ha ignorancia, ha conhecimento. Nao ha paixao, ha
serenidade.Nao ha caos, ha harmonia. Nao ha morte, ha a Forca.
Usamos o método promisify do módulo útil do core do NodeJS para trabalhar
com promises em vez de callbacks.
CAPÍTULO5
Construindo uma API RESTful
com ExpressJS
5.1 ExpressJS
O ExpressJS (http://expressjs.com) é um framework minimalista e flexível
para desenvolvimento web. Nós o utilizaremos para gerenciar as rotas da
nossa aplicação. Crie uma nova pasta para iniciar esse projeto. Vamos utilizar
NodeJS superior a v14 daqui em diante:
$ node -v
v15.5.0
Portanto, crie um arquivo .npmrc:
$ cat .nvmrc
15.5.0
Execute o comando npm init --yes e instale o ExpressJS com a flag --save:
$ npm init --yes
$ npm install express --save
Assim, ele será adicionado ao objeto dependencies do package.json:
"dependencies": {
"express": "^4.17.1"
},
É possível deixar explícito no package.json que só aceitamos versões acima da
14:
"engines": {
"node": ">=14.0.0"
},
Crie uma pasta chamada server na raiz do projeto1 e, dentro dela, o arquivo
server/app.js. Nesse arquivo, nós vamos importar o módulo do ExpressJS com
função require() da mesma forma que fazemos para chamar um módulo nativo
do NodeJS, e aí sim instanciar o Express.
const express = require('express')
const app = express()
Depois disso, vamos declarar uma rota para a raiz.
app.get('/', (req, res) => {
res.send('Ola s')
})
Uma rota é um caminho até um recurso. É onde declaramos em qual endereço
vamos interpretar as requisições que serão enviadas para a nossa aplicação
web, e aí responder o que for solicitado. Com o código anterior, declaramos
uma rota na index, para o verbo HTTP GET.
Agora, podemos indicar em qual porta o nosso servidor manterá um processo
que ficará aberto, aguardando novas conexões.
app.listen(3000)
Este código é bem parecido com o exemplo da documentação do ExpressJS:
http://expressjs.com/en/starter/hello-world.html.
Arquivo server/app.js
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Olas')
})
app.listen(3000)
Declaramos o servidor, configuramos uma rota para o caminho /, iniciamos o
listener na porta 3000 e imprimimos uma mensagem na tela informando o
endereço e a porta. No seu terminal, navegue até o diretório da aplicação e
digite o comando node seguido pelo nome do arquivo que acabou de criar.
$ node server/app.js
Com isso, o servidor já está funcionando. Quando visitarmos no navegador o
endereço: http://localhost:3000/, será mostrada a frase Olas. Agora pode
encerrar o processo com Ctrl + C, nunca pare o processo com Ctrl + Z, essa
combinação na verdade não mata o processo, mas libera o terminal jogando o
processo para background, pois vamos utilizar o nodemon dentro da sessão
scripts do package.json.
Arquivo package.json:
{
"name": "livro",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon server/app",
"test": "echo \"Error: no test specified\" && exit 1"
},
"engines": {
"node": ">=14.0.0"
},
"keywords": [],
"author": "William Bruno <[email protected]> (http://wbruno.com.br)",
"license": "ISC",
"dependencies": {
"express": "^4.17.1"
}
}
Assim que executar npm run dev, ou yarn dev, o Nodemon irá iniciar nosso
servidor e ficará ouvindo as alterações dos arquivos em disco para reiniciar o
processo.
$ npm run dev
> [email protected] dev /Users/wbruno/Sites/wbruno/livro
> nodemon server/app
[nodemon] 2.0.6
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node server/app.js
Vamos utilizar ES6 modules, então, para isso, indicamos "type": "module", no
package.json e trocamos o require por import no arquivo server/app.js. Na Figura
5.1 visualizamos essas modificações utilizando o comando git diff.
5.2 Middlewares
Um middleware (http://expressjs.com/guide/using-middleware.html) é uma
função que intercepta cada requisição que a aplicação recebe, realiza algum
processamento, delega ao próximo middleware o restante da execução, ou
responde, finalizando o ciclo de vida desse request. Um middleware pode:
• executar qualquer código;
• alterar os objetos request e response;
• chamar o próximo middleware da cadeia, por meio da função next;
• terminar o ciclo request response.
Pelo método app.use(), declaramos os middlewares do Express. Toda
requisição é respondida por um callback do tipo:
function (request, response, next) {} ou (request, response, next) => {}
Detalhando cada um dos parâmetros:
Objeto err
Este objeto é um objeto de erro do tipo Error e só é o primeiro argumento do
middleware de erros.
const err = new Error('Something happened');
err.status = 501;
Podemos anexar uma propriedade status para que o nosso manipulador de
erro saiba com qual status code responder a solicitação. Sempre que um new
Error() for disparado, o middleware de erro será invocado pelo ExpressJS,
assim poderemos fazer todos os tratamentos num único ponto do código,
facilitando muito a leitura e o debug da aplicação.
Objeto request
Nesse objeto, temos acesso às informações da solicitação que chegou à
nossa aplicação, como cabeçalho, corpo, método, URL, query string,
parâmetros, user agent, IP etc. Geralmente abreviam request para req.
Conseguimos anexar novas propriedades ou sobrescrever partes do objeto
request para propagar informações entre a cadeia de middlewares.
Objeto response
O objeto response é nosso para manipular da forma que quisermos. Tem
funções para responder à requisição, então conseguimos devolver um status
code, escrever na saída, encerrar, enviar JSON, texto, cabeçalhos, cookies
etc. Você vai encontrar outros códigos por aí, escrito response apenas como
res.2
Função next
Essa função repassa a requisição para o próximo middleware na cadeia,
caso precisemos, por exemplo, manipular alguma coisa do request e então
repassar para outro middleware terminar de responder uma requisição.
favicon.ico
É um comportamento padrão dos navegadores que eles sempre peçam, para
um domínio, o arquivo favicon.ico. O favicon é aquele ícone que fica no lado
esquerdo do nome do título do site, na aba do navegador. Como estamos
escrevendo uma API RESTful, não temos necessidade de servir esse ícone.
Para não entregar sempre um 404 de imagem não encontrada, podemos
devolver um vazio.
app.use((request, response, next) => {
if (request.url === '/favicon.ico') {
response.writeHead(200, {'Content-Type': 'image/x-icon'})
response.end('')
} else {
next()
}
})
Utilizei um middleware para verificar se a URL requisitada foi favicon.ico.
Caso seja, eu devolvo o status code 200, com o cabeçalho do tipo de imagem
.ico, e finalizo a resposta com uma string vazia. Caso contrário, se não for o
favicon que foi solicitado, apenas repasso a requisição para o próximo
manipulador (middleware).
Nesse caso, seria o mesmo que fazer:
app.get('/favicon.ico', (request, response, next) => {
response.writeHead(200, {'Content-Type': 'image/x-icon'})
response.end('')
})
Body Parser
Quando recebemos uma requisição com corpo (POST, PUT ou PATCH) no
NodeJS, ela pode chegar a form url encoded, Form Data ou JSON. Por
padrão, o ExpressJS 4 não entende esses formatos e recebe as requisições
apenas como texto puro.
Então, para que o servidor entenda esses formatos corretamente e já faça o
parser, precisamos avisar à nossa aplicação quais tipos de body ela aceita.
Configurando o app no arquivo server/app.js:
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
Feito isso, o nosso servidor está configurado para trabalhar como uma API
REST. Note o extended: true, que utilizei para que o parser se estenda a objetos
encadeados; do contrário, o parser seria feito apenas no primeiro nível do
corpo da requisição.
Objeto express.Router()
Para organizar as rotas da nossa aplicação em outros arquivos, de maneira
simples, temos disponível o objeto express.Router(). Utilizando-o, podemos
extrair as rotas:
Arquivo server/app.js:
import express from 'express'
import routes from './routes/index.js'
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(routes)
app.use((request, response, next) => {
var err = new Error('Not Found')
err.status = 404
next(err)
})
app.use((err, request, response, next) => {
if (err.status !== 404) console.log(err.stack)
response.status(err.status).json({ err: err.message })
})
app.listen(3000)
A sintaxe para a criação de uma rota é:
<objeto router>.<verbo HTTP>('/<endpoint>/<parâmetro>, (<request>, <response>) =>
{
response.<função para escrever a resposta>
});
Arquivo routes/index.js:
import { Router } from 'express'
const routes = new Router()
routes.get('/', (req, res) => {
res.send('Ola s')
})
routes.get('/favicon.ico', (request, response, next) => {
response.writeHead(200, {'Content-Type': 'image/x-icon'})
response.end('')
})
export default routes
Destaquei em negrito os novos trechos de código. Repare também que
trocamos app.get por routes.get, pois não temos mais acesso à instância do
express, e sim à instância do Router.
Diferentemente do CommonJS, em que o NodeJS procura um arquivo local
do projeto chamado routes.js ou routes/index.js, quando usamos require('./routes'),
quando usamos Modules, devemos informar o caminho completo e exato:
from 'routes/index.js'.
Devemos olhar o arquivo server/app.js reconhecendo essas quatro partes:
Configuração do app
Nessa parte do app.js nós colocamos todos os middlewares de aplicação, de
terceiros e embutidos (built-in), que queremos que afetem todos os requests.
Nessa parte iremos definir o servidor, configurar o que ele faz, quais
recursos ele aceita, como trabalha com cookies, seções etc.
Rotas
Rotas ou roteamento é onde declaramos os endpoints da aplicação. Sempre
devem vir depois de todas as configurações, mas antes do tratamento de
erros.
Tratamento de erros
Eu defino o tratamento de erros e manipulação de 404 como uma área
especial do server/app.js, porque a ordem em que ele será escrito no código é
importante e afeta diretamente a aplicação. O error handling deve ser
declarado após todas as rotas, como último middleware da aplicação.
Listener do servidor
É onde de fato o servidor é declarado, informamos em qual porta ele irá
aceitar as requisições e, mais para a frente, será onde escalaremos a nossa
aplicação verticalmente.
5.3 Controllers
Usaremos a arquitetura MVC (Model, View e Controller) para desenvolver
nossa API. Em nossos arquivos de rotas, ficarão apenas as declarações dos
caminhos e cada um invocará os middlewares ou controller correspondentes.
O controller é responsável por lidar com o request e devolver uma resposta
para quem solicitou.
Arquivo server/app.js
import express from 'express'
import Home from './controller/Home.js'
const app = express()
app.get('/', Home.index)
app.use((request, response, next) => {
var err = new Error('Not Found')
err.status = 404
next(err)
})
app.use((err, request, response, next) => {
if (err.status !== 404) console.log(err.stack)
response.status(err.status).json({ err: err.message })
})
export default app
E o novo arquivo server/controller/Home.js:
const Home = {
index (request, response) {
response.json({ 'name': 'William Bruno', 'email': '[email protected]' })
}
}
export default Home
Note que agora a declaração da rota está bem simples. Apenas declara os
endpoints e delega a responsabilidade de lidar com o request para um método
do controller. Vamos refatorar os middlewares de erro também.
Arquivo server/app.js
import express from 'express'
import Home from './controller/Home.js'
import AppController from './controller/App.js'
const app = express()
app.get('/', Home.index)
app.use(AppController.notFound)
app.use(AppController.handleError)
export default app
O controller para esses últimos middlewares fica dessa forma:
Arquivo server/controller/App.js
const AppController = {
notFound(request, response, next) {
var err = new Error('Not Found')
err.status = 404
next(err)
},
handleError(err, request, response, next) {
if (err.status !== 404) console.log(err.stack)
response.status(err.status || 500).json({ err: err.message })
}
}
export default AppController
Organizando o nosso código dessa forma, temos as responsabilidades bem
divididas nas camadas corretas do MVC.
5.4.1 Cluster
O NodeJS não abre uma nova thread para cada requisição que recebe, isso faz
com que ele seja muito mais escalável em uma situação de alto tráfego de
entrada e saída. Vale lembrar que existe um limite para a quantidade de
threads que podem ser abertas, já que é alocado um espaço na memória da
máquina para cada nova thread, que fica bloqueada aguardando uma resposta.
Felizmente o NodeJS surgiu com uma proposta diferente, em que todas as
requisições chegam a um único processo que não fica bloqueado aguardando
a resposta e, portanto, pode continuar recebendo novas requisições sem
bloquear nem aguardar uma resposta de quem ele tiver solicitado. O Event
Loop é que avisa o término de uma consulta no banco, leitura do disco,
requisição externa etc., enquanto o processo principal continua desbloqueado
para continuar recebendo novas entradas, consumindo muito menos memória
que na arquitetura: uma requisição, uma thread.
Uma instância de um processo NodeJS roda em apenas uma única thread do
processador, mas podemos instanciar um processo NodeJS para cada thread,
fazendo, assim, um paralelismo real, pois haverá um processo não bloqueante
para cada executor do processador.
A saída no terminal é:
livro_nodejs:www server started +0ms
Mostra uma única linha do comando debug, pois apenas uma thread do
processador recebeu a instância do servidor.
O módulo cluster (https://nodejs.org/api/cluster.html) permite que escalemos
o NodeJS verticalmente, subindo um processo para cada núcleo da máquina.
Convém lembrar que o processador da máquina não é capaz de paralelismo
real. Ele só processa uma coisa de cada vez, uma depois de terminar a
anterior. O NodeJS representa isso de forma transparente, ao ser single-
thread, assíncrono e não bloqueante, graças a libuv
(https://github.com/libuv/libuv).
Para escalar a aplicação verticalmente, iremos alterar o arquivo onde fica o
listener do servidor.
Arquivo server/bin/www
#!/usr/bin/env node
import app from '../app.js'
import debug from 'debug'
import cluster from 'cluster'
import os from 'os'
const cpus = os.cpus()
const log = debug('livro_nodejs:www')
if (cluster.isMaster) {
cpus.forEach(_ => cluster.fork())
cluster.on('exit', (err) => log(err))
} else {
app.listen(3000, () => log('server started'))
}
A saída no terminal ao executar o npm run dev agora é:
$ npm run dev
> export DEBUG=livro_nodejs:* &&nodemon server/bin/www
[nodemon] 2.0.6
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node server/bin/www.js`
livro_nodejs:www server started +0ms
livro_nodejs:www server started +0ms
livro_nodejs:www server started +0ms
livro_nodejs:www server started +0ms
livro_nodejs:www server started +0ms
livro_nodejs:www server started +0ms
livro_nodejs:www server started +0ms
livro_nodejs:www server started +0ms
Apareceu oito vezes o debug() porque a minha máquina possui quatro núcleos
e oito threads. Temos agora nove processos NodeJS:
$ ps aux | grep node
wbruno 1771 ... /Users/wbruno/...node /.../server/bin/www.js
wbruno 1770 ... /Users/wbruno/...node /.../server/bin/www.js
wbruno 1769 ... /Users/wbruno/...node /.../server/bin/www.js
wbruno 1768 ... /Users/wbruno/...node /.../server/bin/www.js
wbruno 1767 ... /Users/wbruno/...node /.../server/bin/www.js
wbruno 1766 ... /Users/wbruno/...node /.../server/bin/www.js
wbruno 1765 ... /Users/wbruno/...node /.../server/bin/www.js
wbruno 1764 ... /Users/wbruno/...node /.../server/bin/www.js
wbruno 1763 ... /Users/wbruno/...node /.../server/bin/www.js
Ocultei algumas informações do retorno para facilitar a explicação. O
processo com o id 1763, que é o menor número dentre esses que retornaram,
foi o primeiro core da máquina a receber o comando, portanto esse é o
master. Os demais – 1764, 1765, 1766, 1767, 1768, 1769, 1770 e 1771 – são
os workers.
O master é responsável por balancear as requisições e distribuir para o worker
que estiver livre. Para isso, ele utiliza o algoritmo round robin.3 Caso precise
escalar mais, use mais máquinas com um load balancer na frente delas,
escalando, assim, horizontalmente.
Recuperação de falhas
Um worker pode morrer ou cometer suicídio. Uma exceção não tratada, uma
requisição assíncrona sem catch ou um erro de sintaxe são falhas graves,
capazes de matar um worker. Em uma situação dessas é interessante que a
aplicação consiga se recuperar e não saia do ar, pelo menos até você
descobrir o motivo de os workers estarem morrendo e corrigir o código,
tratando corretamente a exceção.
Cada vez que um worker morre, é emitido um evento, e você pode fazer um
novo fork para que a aplicação não fique sem processos aptos a responder às
requisições.
Arquivo bin/www
#!/usr/bin/env node
import app from '../app.js'
import debug from 'debug'
import cluster from 'cluster'
import os from 'os'
const cpus = os.cpus()
const log = debug('livro_nodejs:www')
const onWorkerError = (code, signal) => log(code, signal)
if (cluster.isMaster) {
cpus.forEach(_ => {
const worker = cluster.fork()
worker.on('error', onWorkerError);
})
cluster.on('exit', (err) => {
const newWorker = cluster.fork()
newWorker.on('error', onWorkerError)
log('A new worker rises', newWorker.process.pid)
})
cluster.on('exit', (err) => log(err))
} else {
const server = app.listen(3000, () => log('server started'))
server.on('error', (err) => log(err))
}
Assim, após iniciar o servidor com npm dev run, se em outra aba do terminal
eu matar um worker com kill <process_pid>, terei o seguinte log no terminal:
$ npm run dev
…
livro_nodejs:www A new worker rises +0ms 7851
livro_nodejs:www server started +0ms
Ou seja, quando eu matei um processo com o comando kill, um novo com o
pid 7851 surgiu para tomar lugar daquele.
5.4.2 dnscache
O módulo dnscache (https://github.com/yahoo/dnscache) foi criado e é
mantido pela equipe do Yahoo, para cachear as resoluções de DNS. Uma
resolução de DNS é o processo em que cada domínio é convertido para um IP
antes de ser acessado.
O NodeJS não guarda o resultado dessa resolução, então, cada vez que você
fizer um request para uma API, o NodeJS transforma novamente aquele
domínio em um IP, mesmo que seja exatamente o mesmo destino anterior.
Esse processo costuma ser extremamente rápido, mas em uma situação de
alta carga, em que sua aplicação faz diversas chamadas, isso se torna custoso
no tempo de resposta final, e para o sistema operacional também, devido à
elevada quantidade de resoluções.
Outras linguagens, como Java e PHP, já fazem cache de DNS por padrão,
mas o NodeJS e o Python não. Para isso, basta instalar:
$ npm i --save dnscache
E depois configurar quanto tempo de cache no arquivo server/bin/www:
import dns from 'dns'
import dnscache from 'dnscache'
dnscache({
"enable" : true,
"ttl" : 300,
"cachesize" : 1000
})
package.json
O package.json é o arquivo de definições de um projeto NodeJS. Contém a
lista das dependências, nome, versão, url do Git etc.
config
A pasta config contém os arquivos de configuração. Nesses arquivos
colocamos dados da conexão com os bancos de dados, URLs de web
services etc. Enfim, são informações ou endereços. Os arquivos de
configuração não têm nenhuma lógica, por isso são arquivos .json.
server/bin/www
Na pasta server/bin colocamos o programa que será chamado pela linha de
comando, o ponto de entrada para executar a aplicação. Como estamos
escrevendo uma API RESTful, é o arquivo server/bin/www que contém o
listener do servidor HTTP. Esse comportamento deve ficar isolado do
restante da configuração do ExpressJS. Será nele que iremos escalar o
NodeJS verticalmente, adicionando o comportamento de cluster, subindo
um processo NodeJS para cada core do processador da máquina.
server/app.js
O arquivo app.js é onde fica a configuração do ExpressJS. Esse arquivo
exporta uma variável chamada app, por isso se chama app.js. Dessa forma, os
testes podem utilizar o app sem o efeito colateral do listener do servidor, que
está isolado na pasta bin/www.
server/config/mongoist.js
Na pasta server/config eu coloco uma abstração para os bancos de dados que
vou utilizar. É onde fica o tratamento de erros caso o banco caia ou a
conexão falhe, por exemplo. Esse também é o único ponto da aplicação que
sabe usar a config de dados do banco para conectar com ele.
Frequentemente, temos que nos conectar com mais de um banco de dados,
por isso é uma boa prática ter todas as conexões centralizadas num mesmo
ponto.
server/controller
A pasta controllers é o local onde colocamos o C do MVC. Um controller é
responsável por entender o que o usuário solicitou no request, repassar esse
pedido para algum Model ou Service e retornar uma resposta.
server/repository
O M do MVC. Um model é responsável por regras de negócio e por
representar nossas entidades. Note que, por questões de performance, em
NodeJS não utilizaremos o pattern ActiveRecord. Prefiro utilizar o Design
Pattern DAO, por implicar menor consumo de memória e maior
simplicidade.
public
São os arquivos estáticos: imagens, CSS e JS client-side. Essa pasta
idealmente não é servida pelo NodeJS, pois queremos que o NodeJS se
preocupe com tarefas dinâmicas, como consultar algo no banco de dados e
não entregar um arquivo estático. O Nginx é mais rápido e consome menor
memória para servir estáticos. Esses arquivos não serão processados, por
isso os chamamos de estáticos.
server/routes
Na pasta routes fica a definição dos endpoints. Uma rota está associada a um
método do controller. Pode parecer desnecessária essa divisão por
enquanto, mas, quando adicionarmos autenticação e regras de ACL (Acess
Control List), o routes ficará com mais responsabilidades, por isso é uma
boa ideia separar.
tests/unit
São os testes que fazemos método por método, comportamento por
comportamento. Nesse diretório, vamos praticamente repetir a estrutura do
projeto. Haverá as pastas controllers, models etc. É importante lembrar que
um teste unitário não depende de nada, nem de serviços externos, nem de
banco, nem do teste anterior. Cada teste deve rodar isoladamente e não
influenciar o próximo teste.
tests/integration
São testes caixas-pretas, em que fingimos não conhecer o código-fonte.
Faremos requisições HTTP nas rotas da API sem nos preocupar com cada
método de cada arquivo, mas sim com cada rota e com o que ela pode fazer.
Esses testes visam aferir a integração da aplicação com as dependências
dela. Eles utilizam bancos de dados e tudo mais de que precisarem. Porém,
a regra de que um teste não pode afetar outro continua valendo, por isso
limpamos o banco após cada teste e criamos os dados necessários para que
cada cenário possa ser independente.
views
As views são os arquivos HTML do template, a camada de visualização que
iremos apresentar para o usuário. Como esses arquivos serão processados
pelo template engine, portanto são dinâmicos, então eles não ficam dentro
da public.
5.5.1 mongoist
O model é a camada responsável pelos dados, pela validação e consistência
deles. Utilizaremos repositories para acessar o banco de dados. Começaremos
com o mongoist.
Crie o arquivo server/config/mongoist.js para conectar no banco de dados e fazer
o handler de erros de conexão:
Arquivo server/config/mongoist.js
import debug from 'debug'
import mongoist from 'mongoist'
const log = debug('livro_nodejs:config:mongoist')
const db = mongoist('mongodb://localhost:27017/livro_nodejs')
db.on('error', (err) => log('mongodb err', err))
export default db
Entretanto, não é uma boa prática os dados de conexão serem declarados
diretamente no código-fonte do projeto, pois esses dados variam de acordo
com o ambiente em que a aplicação vai estar, por exemplo, em nossa
máquina local o MongoDB está instalado no localhost, mas no servidor de
produção precisaremos informar outro endereço, assim como um usuário e
uma senha.
Por isso, utilizaremos o módulo node-config
(https://github.com/lorenwest/node-config) para que essas informações
fiquem isoladas do código da aplicação e tenhamos uma forma simples de
gerenciar configurações por ambiente. Crie o arquivo config/default.json com o
seguinte conteúdo:
Arquivo config/default.json
{
"mongo": {
"uri": "mongodb://localhost:27017/livro_nodejs"
}
}
Instale o node-config como uma dependência do projeto:
$ npm i --save config
E altere o arquivo server/config/mongoist.js para que ele puxe os dados de
conexão por meio do módulo de config:
Arquivo refatorado server/config/mongoist.js
import debug from 'debug'
import mongoist from 'mongoist'
import config from 'config'
const log = debug('livro_nodejs:config:mongoist')
const db = mongoist(config.get('mongo.uri'))
db.on('error', (err) => log('mongodb err', err))
export default db
Com essa arquitetura, podemos trocar o servidor do banco de dados sem
mexer nos códigos da aplicação. Os arquivos de configuração contêm apenas
informações e dados, sem nenhuma lógica. E o repository importa o arquivo
de conexão com a base de dados.
Arquivo server/repository/Stormtrooper.js
import db from '../config/mongoist.js'
const Stormtrooper = {
list() {
const query = {}
return db.stormtroopers.find(query)
}
}
export default Stormtrooper
Agora o nosso controller pode utilizar o model em
server/controllers/Stormtrooper.js:
Arquivo server/controllers/Stormtrooper.js
import repository from '../repository/Stormtrooper.js'
const Stormtrooper = {
list(request, response, next) {
repository.list()
.then(result => response.json(result))
.catch(next)
},
byId(request, response, next) {},
create(request, response, next) {},
updateById(request, response, next) {},
deleteById(request, response, next) {}
}
export default Stormtrooper
Para construir o CRUD de soldados, precisaremos de cinco rotas:
• GET no endpoint /troopers, que nos retornará uma lista de todos os
registros do banco;
• GET no endpoint /troopers/:id, que nos retornará apenas um único registro
selecionado pelo id;
• POST no endpoint /troopers irá cadastrar um novo soldado;
• PUT no endpoint /troopers/:id atualizará as informações de um soldado;
• DELETE no endpoint /troopers/:id removerá esse soldado do banco de
dados.
Para isso, criaremos um novo arquivo server/routes/troopers.js.
Vamos definir qual rota e método HTTP delega para cada controller.
Arquivo server/routes/trooper.js
import { Router } from 'express'
import controller from '../controller/Stormtrooper.js'
const trooperRoutes = new Router()
trooperRoutes.get('/', controller.list)
trooperRoutes.get('/:id', controller.byId)
trooperRoutes.post('/', controller.create)
trooperRoutes.put('/:id', controller.updateById)
trooperRoutes.delete('/:id', controller.deleteById)
export default trooperRoutes
E o arquivo de rotas principal foi modificado para suportar a separação do
server/routes/troopers.js:
Arquivo server/routes/index.js
import { Router } from 'express'
import trooperRoutes from './troopers.js'
const routes = new Router()
routes.use('/troopers', trooperRoutes)
export default routes
Lembrando que, no server/app.js, temos uma chamada do server/routes/index.js.
Arquivo server/app.js
import express from 'express'
import routes from './routes/index.js'
const app = express()
app.use(routes)
export default app
Revisando o package.json, vemos os quatro módulos que instalamos.
Arquivo package.json
{
"name": "livro",
"type": "module",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "export DEBUG=livro_nodejs:* &&nodemon server/bin/www",
"test": "echo \"Error: no test specified\" && exit 1"
},
"engines": {
"node": ">=14.0.0"
},
"keywords": [],
"author": "William Bruno <[email protected]> (http://wbruno.com.br)",
"license": "ISC",
"dependencies": {
"config": "^3.3.3",
"debug": "4.3.1",
"dnscache": "^1.0.2",
"express": "^4.17.1",
"mongoist": "^2.5.3"
}
}
Ao invocar a rota GET /troopers, devemos ver a listagem dos soldados que
existem no banco de dados:
$ curl 'http://localhost:3000/troopers'
[{"_id":"5fee0a86eaa0d28eea176f70","name":"CT-
5555","nickname":"Fives","divisions":["Coruscant Guard"],"patent":"Soldier"}]
GET /troopers/:id
Para retornar um soldado pelo id, usaremos a rota:
trooperRoutes.get('/:id', Stormtrooper.byId)
E o controller:
byId(request, response, next) {},
que irá invocar a função correspondente do repository dessa forma:
byId(request, response, next) {
repository.byId(request.params.id)
.then(result => response.json(result))
.catch(next)
},
E o método para acessar o banco de dados:
byId(id) {
return db.stormtroopers.findOne({ _id: mongoist.ObjectId(id) })
},
Note que a query é { _id: mongoist.ObjectId(id) }, pois precisamos transformar a
string recebida como parâmetro da URI em um objeto ObjectId para o banco
encontrar o document.
Uma validação que podemos fazer é retornar um Não Encontrado caso seja
solicitado um ID que não existe. Para isso, vamos modificar apenas o
controller.
byId(request, response, next) {
repository.byId(request.params.id)
.then(result => {
if (!result) {
const err = new Error('trooper not found')
err.status = 404
return next(err)
}
return result
})
.then(result => response.json(result))
.catch(next)
},
Ao tentar pesquisar por um id que não existe, como
‘http://localhost:3000/troopers/5fffffffffffffffffffffff’, teremos um 404.
O objeto ObjectId deve ser uma string hexadecimal de 24 caracteres, então,
podemos validar isso também antes de invocar o repository.
const id = request.params.id
if (!/^[0-9a-f]{24}$/.test(id)) {
const err = new Error('invalid id')
err.status = 422;
return next(err)
}
Executando no curl:
$ curl 'http://localhost:3000/troopers/xpto' --head
HTTP/1.1 422 Unprocessable Entity
Já que estamos retornando um objeto Error para a função next, podemos usar o
modulo http-errors (https://github.com/jshttp/http-errors) para simplificar o
nosso código.
$ npm i --save http-errors
Fica assim o controller até agora:
import repository from '../repository/Stormtrooper.js'
import createError from 'http-errors'
const handleNotFound = (result) => {
if (!result) {
throw createError(404, 'trooper not found')
}
return result
}
const Stormtrooper = {
list(request, response, next) {
repository.list()
.then(result => response.json(result))
.catch(next)
},
byId(request, response, next) {
const id = request.params.id
if (!/[0-9a-f]{24}/.test(id)) {
return next(createError(422, 'invalid id'))
}
repository.byId(id)
.then(handleNotFound)
.then(result => response.json(result))
.catch(next)
},
create(request, response, next) {},
updateById(request, response, next) {},
deleteById(request, response, next) {}
}
export default Stormtrooper
Aproveitei para criar a função handleNotFound e extrair aquela lógica de dentro
do controller. Reescrevendo para async/await, ficaria dessa forma:
async byId(request, response, next) {
const id = request.params.id
if (!/^[0-9a-f]{24}$/.test(id)) {
return next(createError(422, 'invalid id'))
}
try {
const result = await repository.byId(id)
.then(handleNotFound)
response.json(result)
} catch(e) {
next(e)
}
},
Colocamos a palavra async antes do método byId, pois iremos utilizar o await
no retorno da promise. Por esse motivo precisamos colocar o try/catch em
volta da chamada assíncrona do repository.
Tendo a validação de id, podemos reutilizá-la para os métodos GET :id, PUT
:id, e DELETE :id, logo, fica legal colocar como um middleware, extraindo
para uma função:
const verifyId = (request, response, next) => {
const id = request.params.id
if (!/[0-9a-f]{24}/.test(id)) {
return next(createError(422, 'invalid id'))
}
next()
}
E reutilizar nas rotas:
import { Router } from 'express'
import createError from 'http-errors'
import controller from '../controller/Stormtrooper.js'
const trooperRoutes = new Router()
const verifyId = (request, response, next) => {
const id = request.params.id
if (!/^[0-9a-f]{24}$/.test(id)) {
return next(createError(422, 'invalid id'))
}
next()
}
trooperRoutes.get('/', controller.list)
trooperRoutes.get('/:id', verifyId, controller.byId)
trooperRoutes.post('/', controller.create)
trooperRoutes.put('/:id', verifyId, controller.updateById)
trooperRoutes.delete('/:id', verifyId, controller.deleteById)
export default trooperRoutes
Podemos, agora, deixar o controller mais limpo:
async byId(request, response, next) {
const id = request.params.id
try {
const result = await repository.byId(id)
.then(handleNotFound)
response.json(result)
} catch(e) {
next(e)
}
},
Vamos implementar o create, no controller, que é apenas uma chamada ao
repository:
create(request, response, next) {
repository.create(request.body)
.then(result => response.status(201).json(result))
.catch(next)
},
E no repository:
create(data) {
return db.stormtroopers.insert(data)
}
Estamos apenas recebendo os dados do body e inserindo no mongo, sem
nenhuma validação, depois melhoraremos isso. Para testar no terminal,
usando curl:
$ curl 'http://localhost:3000/troopers' -H 'content-type: application/json' -d '{"name":
"TK-132137"}'
{"name":"TK-132137","_id":"5ff0710b156a1f6e82180a49"}
Ou no Insomnia, do lado esquerdo coloco o método HTTP, o endpoint da
API e os dados, exemplificado na Figura 5.2:
{
"name": "CT-1321",
"patent": "Lieutneant",
"divisions": [
"First regiment"
]
}
Filtros
Usamos query strings para filtrar os recursos retornados. Modificaremos a
rota GET /troopers que é o list, para entender o que foi enviado e repassar esse
filtro para o repository; por exemplo, se quisermos fazer um autocomplete
pelo nome dos stormtroopers, precisamos ir filtrando letra por letra digitada:
GET /troopers?q=c, GET /troopers?q=ct, GET /troopers?q=ct-10, e daí em diante.
Enviaremos o q no controller:
list(request, response, next) {
repository.list(request.query.q)
.then(result => response.json(result))
.catch(next)
},
E, no repository, transformamos esse parâmetro em uma expressão regular,
para colocar na propriedade name da query:
list(q) {
const query = {}
if (q) query.name = new RegExp(q, 'i')
return db.stormtroopers.find(query)
},
Paginação
Para implementar paginação, precisamos apenas limitar a quantidade de itens
retornados e ser capazes de pular uma certa quantidade deles. No MongoDB,
usamos limit e skip para isso, respectivamente. No controller, apenas
repassamos mais um parâmetro:
const { q, page } = request.query
repository.list(q, page)
.then(result => response.json(result))
.catch(next)
},
O endpoint agora será invocado dessas maneiras: GET /troopers, GET /troopers?
page=1, GET /troopers?page=3. No repository, definimos um valor padrão, caso o
endpoint seja chamado sem nenhum valor de página, e faremos uma simples
conta multiplicando a quantidade de itens pela página:
list(q, page = 1) {
const query = {}
if (q) query.name = new RegExp(q, 'i')
const DEFAULT_LIMIT = 3
const skip = Math.abs(page - 1) * DEFAULT_LIMIT
return db.stormtroopers.find(query, {}, { skip, limit: DEFAULT_LIMIT })
},
O segundo argumento da função find() recebe os campos que queremos trazer
do banco, como quero todos, passei apenas um objeto vazio {}, e o terceiro
argumento recebe opções como skip e limit. Utilizei o Math.abs para pegar o
valor absoluto, ou seja, ignorar valores negativos, mas algum outro
tratamento mais fino ficaria melhor aqui.
5.5.2 Mongoose
O módulo Mongoose (https://mongoosejs.com) é um ODM (Object
Document Model) para MongoDB. Provê validação, conversão de tipo,
camada de negócio e lhe dá um schema para trabalhar.
Utilizamos o mongoist para nos conectar no MongoDB e fazer o CRUD da
API, só que não fizemos nenhuma validação. Agora, vamos trocar o
mongoist pelo Mongoose e colocar isso.
Instalaremos o Mongoose como dependência:
$ npm rm --save mongoist
$ npm install --save mongoose
Criaremos um arquivo de conexão:
Arquivo server/config/mongoose.js
import debug from 'debug'
import mongoose from 'mongoose'
import config from 'config'
const log = debug('livro_nodejs:config:mongoose')
mongoose.connect(config.get('mongo.uri'), { useNewUrlParser: true,
useUnifiedTopology: true })
mongoose.connection.on('error', (err) => log('mongodb err', err))
export default mongoose
Temos um schema para definir e validar as propriedades da entidade.
Arquivo server/schema/Stormtrooper.js
import mongoose from '../config/mongoose.js'
const { Schema } = mongoose
const Stormtrooper = new Schema({
name: String,
nickname: String,
divisions: [ String ],
patent: {
type: String,
enum: ['General', 'Colonel', 'Major', 'Captain', 'Lieutenant', 'Sergeant', 'Soldier']
}
})
export default Stormtrooper
E o repository, que refatoramos para, em vez de usar o mongoist, usar o
mongoose:
Arquivo server/repository/Stormtrooper.js
import mongoose from '../config/mongoose.js'
import schema from '../schema/Stormtrooper.js'
const model = mongoose.model('Stormtrooper', schema)
const Stormtrooper = {
list() {
const query = {}
return model.find(query)
},
byId(id) {
return model.findOne({ _id: id })
},
create(data) {
const trooper = new model(data)
return trooper.save()
},
updateById(id, data) {
return model.updateOne({ _id: id }, data)
},
deleteById(id) {
return model.deleteOne({ _id: id })
},
}
export default Stormtrooper
Ressaltei em negrito as alterações. Veja que, como a interface não foi
alterada, não precisamos mexer em absolutamente nada do controller. Essa é
a grande vantagem dessa arquitetura, por termos camadas bem definidas e
isoladas, é muito simples trocar a persistência.
5.5.3 pg
Utilizando o módulo node postgres (https://node-postgres.com), podemos
conectar no Postgres em vez de no MongoDB e, devido à arquitetura que
utilizamos, só precisamos mexer no repository.
$ npm rm --save mongoist
$ npm i --save pg
Iremos usar um número inteiro como tipo do id no Postgres; portanto, a
validação no arquivo de rotas precisa mudar para:
const verifyId = (request, response, next) => {
const id = request.params.id
if (!/^[0-9]+$/.test(id)) {
return next(createError(422, 'invalid id'))
}
next()
}
A regex mudou de /^[0-9a-f]{24}$/ para /^[0-9]+$/, assim só aceitamos números.
Podemos criar o arquivo de conexão.
Arquivo server/config/pg.js
import pg from 'pg'
import debug from 'debug'
const log = debug('livro_nodejs:config:pg')
const pool = new pg.Pool({
user: 'wbruno',
password: '',
host: 'localhost',
port: 5432,
database: 'livro_nodejs',
max: 5
})
pool.on('error', (err) => log('postgres err', err))
export default pool
e adaptar o repository para trabalhar com Postgres:
Arquivo server/repository/Stormtrooper.js
import db from '../config/pg.js'
const sql = `SELECT
st.id, st.name, st.nickname,
p.name as patent
FROM stormtroopers st
JOIN patents p ON p.id = st.id_patent`
const Stormtrooper = {
list() {
return db.query(sql)
.then(result => result.rows)
},
byId(id) {
return db.query(`${sql} WHERE st.id = $1::int`, [id])
.then(result => result.rows && result.rows[0])
},
create(data) {
const sql = `INSERT INTO stormtroopers (name, nickname, id_patent)
VALUES ($1::text, $2::text, $3::int)
RETURNING id`
const params = [data.name, data.nickname, data.id_patent]
return db.query(sql, params)
.then(result => this.byId(result.rows[0].id))
},
updateById(id, data) {
const sql = `UPDATE stormtroopers SET
name = $1::text,
nickname = $2::text,
id_patent = $3::int
WHERE id = $4::int`
const params = [data.name, data.nickname, data.id_patent, id]
return db.query(sql, params)
},
deleteById(id) {
return db.query(`DELETE FROM stormtroopers WHERE id = $1::int`, [id])
},
}
export default Stormtrooper
Filtro
Para filtrar, usaremos ILIKE:
list(q = '') {
const where = q ? `WHERE st.name ILIKE '%' || $1::text || '%'` : ` WHERE
$1::text = ''`
return db.query(`${sql} ${where}`, [q])
.then(result => result.rows)
},
Paginação
O conceito é o mesmo que vimos no MongoDB, só que, no Postgres,
usaremos SQL:
list(q = '', page = 1) {
const DEFAULT_LIMIT = 3
const skip = Math.abs(page - 1) * DEFAULT_LIMIT
const where = q ? `WHERE st.name ilike '%' || $1::text || '%'` : ` WHERE $1::text = ''`
return db.query(`${sql} ${where} LIMIT ${DEFAULT_LIMIT} OFFSET ${skip}`,
[q])
.then(result => result.rows)
},
Lembrando que podemos filtrar e paginar ao mesmo tempo: GET /troopers?
q=ct&page=2, só depende de haver registros suficientes na base.
Cache
Para usar o Redis como cache, instalamos o pacote node-redis
(https://github.com/NodeRedis/node-redis):
$ npm i redis --save
com o seguinte arquivo de conexão:
Arquivo server/config/redis.js
import { createClient } from 'redis';
import { promisify } from 'util';
const client = createClient({
host: 'localhost',
port: 6379
})
client.on('error', (e) => console.log(e))
export const getAsync = promisify(client.get).bind(client)
export const setAsync = promisify(client.set).bind(client)
Criamos um middleware fromCache, que verifica se já existe o valor cacheado
no Redis e assim já retornar sem precisar fazer a query no banco de dados;
caso não o encontre, ou dê algum erro, prossiga para verificar no banco.
Arquivo server/routes/trooper.js
import { Router } from 'express'
import createError from 'http-errors'
import controller from '../controller/Stormtrooper.js'
import { getAsync } from '../config/redis.js'
const trooperRoutes = new Router()
const verifyId = (request, response, next) => {
const id = request.params.id
if (!/^[0-9]+$/.test(id)) {
return next(createError(422, 'invalid id'))
}
next()
}
const fromCache = (request, response, next) => {
getAsync(`trooper:${request.params.id}`)
.then(result => {
if (!result) return next()
response.send(JSON.parse(result))
})
.catch(_ => next())
}
trooperRoutes.get('/', controller.list)
trooperRoutes.get('/:id', verifyId, fromCache, controller.byId)
trooperRoutes.post('/', controller.create)
trooperRoutes.put('/:id', verifyId, controller.updateById)
trooperRoutes.delete('/:id', verifyId, controller.deleteById)
export default trooperRoutes
No repositório, temos que gravar a informação.
Trecho do arquivo server/repository/Stormtrooper.js
byId(id) {
return db.query(`${sql} WHERE st.id = $1::int`, [id])
.then(result => result.rows && result.rows[0])
.then(result => {
const SIX_MINUTES = 60 * 6
setAsync(`trooper:${id}`, JSON.stringify(result), 'EX', SIX_MINUTES)
.catch(e => console.log(e))
return result
})
},
Definimos por quanto tempo a chave vai existir como seis minutos; após esse
tempo, o próprio Redis se encarrega de apagar a chave. Caso não tenhamos
informado nada, a chave não teria expiração nenhuma.
Agora, ao fazer um request:
$ curl 'http://localhost:3000/troopers/1'
a chave correspondente será gravada:
$ redis-cli
127.0.0.1:6379> keys *
1) "trooper:1"
127.0.0.1:6379> get "trooper:1"
"{\"id\":1,\"name\":\"CC-1010\",\"nickname\":\"Fox\",\"patent\":\"Commander\"}"
127.0.0.1:6379> ttl "trooper:1"
(integer) 354
Enquanto o TTL não tiver acabado, continuaremos retornando os dados do
Redis, sem ter feito queries no Postgres. O tempo ideal de TTL varia de
aplicação para aplicação e o quão quentes as informações precisam ser
respondidas.
5.6 Autenticação
Nem sempre tudo pode ser completamente público, por isso precisamos
adicionar uma camada de autenticação em nossa aplicação.
Existem vários tipos e diversas formas de autenticação, desde uma
proprietária, em que você verifica se o usuário digitou a senha correta no seu
banco de dados, até aquelas baseadas em token ou integradas com sistemas
de terceiros, como o social login.
5.6.1 PassportJS
O módulo passportjs (http://passportjs.org) é um middleware de autenticação
não obstrutivo para NodeJS. Ele foi escrito com base no design pattern
Strategy. Cada tipo de autenticação é um strategy do passport. Por exemplo,
se você quiser adicionar autenticação via Facebook, basta utilizar o passport e
o strategy passport-facebook (https://github.com/jaredhanson/passport-
facebook).
Existem estratégias para os mais diversos tipos de autenticação, os quais você
pode usar em conjunto.
• passport-facebook (https://github.com/jaredhanson/passport-facebook);
• passport-twitter (https://github.com/jaredhanson/passport-twitter);
• passport-linkedin (https://github.com/jaredhanson/passport-linkedin);
• passport-google (https://github.com/jaredhanson/passport-google-
oauth2);
• passport-apple (https://github.com/ananay/passport-apple);
• passport-github (https://github.com/jaredhanson/passport-github);
• passport-ldapauth (https://github.com/vesse/passport-ldapauth);
• passport-http (https://github.com/jaredhanson/passport-http);
• passport-local (https://github.com/jaredhanson/passport-local).
Vamos adicionar uma autenticação conhecida como Basic Auth. É aquela
que, quando você tentar acessar uma rota protegida, solicita um usuário e
uma senha. Tecnicamente, esse tipo de autenticação não necessita nem de
banco de dados. A forma de aplicar as outras estratégias é bem parecida, por
isso vou explicar somente esta neste livro.
Instale o passport e o passport-http.
$ npm install passport passport-http --save
Importe no arquivo principal de rotas:
import passport from 'passport'
import { BasicStrategy } from 'passport-http'
O passport provê um middleware para inicializar, e precisamos customizar
como validamos se o usuário e a senha informados estão corretos. Em nosso
caso, o usuário é rebels e a senha é 1138.
routes.use(passport.initialize())
passport.use(
new BasicStrategy((username, password, done) => {
if (username.valueOf() === 'rebels' && password.valueOf() === '1138') {
return done(null, true)
}
return done(null, false)
})
)
Por estar utilizando basic auth, e o header da requisição vir com o cabeçalho
de autenticação, não utilizaremos um controle de sessão. Então
adicionaremos o middleware nas rotas que queremos proteger:
routes.use('/troopers', passport.authenticate('basic', { session: false }), trooperRoutes)
Ao acessar http://localhost:3000/troopers no navegador, será aberta uma
caixa de diálogo para que sejam digitados o usuário rebels e a senha 1138. Se
outra combinação incorreta for digitada, o acesso não será liberado, e
veremos um “401 Unauthorized”.
Arquivo server/routes/index.js
import { Router } from 'express'
import trooperRoutes from './trooper.js'
import passport from 'passport'
import { BasicStrategy } from 'passport-http'
const routes = new Router()
routes.get('/', (req, res) => res.send('Ola s'))
routes.use(passport.initialize())
passport.use(
new BasicStrategy((username, password, done) => {
if (username.valueOf() === 'rebels' && password.valueOf() === '1138') {
return done(null, true)
}
return done(null, false)
})
)
routes.use('/troopers', passport.authenticate('basic', { session: false }), trooperRoutes)
export default routes
Para acessar as rotas, agora precisamos informar usuário e senha:
curl -u rebels:1138 \
-X POST 'http://localhost:3000/troopers' \
-H 'content-type: application/json' \
-d '{"name":"CT-55","patent": "General"}'
{"name":"CT-55","patent":"General","_id":"5ff1ec4962da131b03c06982"}
E para fazer o GET por id:
$ curl -u rebels:1138 'http://localhost:3000/troopers/5ff1ec4962da131b03c06982'
{"_id":"5ff1ec4962da131b03c06982","name":"CT-55","patent":"General"}
Arquivo server/routes/index.js
import { Router } from 'express'
import trooperRoutes from './trooper.js'
import createError from 'http-errors'
import jwt from 'jwt-simple'
import moment from 'moment'
import config from 'config'
const routes = new Router()
routes.get('/', (req, res) => res.send('Ola s'))
routes.post('/login', (request, response, next) => {
const { username, password } = request.body
if (username === 'rebels' && password === '1138') {
const token = jwt.encode({
user: username,
exp: moment().add(7, 'days').valueOf()
}, config.get('jwtTokenSecret'))
return response.json({ token })
}
next(createError(401, 'Unauthorized'))
})
routes.use('/troopers', trooperRoutes)
export default routes
Essa rota /login verifica o usuário e a senha enviados por corpo da requisição
POST e devolve um token:
$ curl -d '{"username":"rebels","password":"1138"}' -H 'content-type: application/json'
http://localhost:3000/login
{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoicmViZWxzIiwiZX
hwIjoxNjEwMjk2NjA0NzI1fQ.0kG9cAZUMXw5axEF3REcIZVgRvfZqcx0orFNxR3r1lE"}
Criaremos um middleware verifyJwt; se o token não for informado,
retornaremos um erro 401:
const verifyJwt = (request, response, next) => {
const token = request.query.token
if (!token) {
return next(createError(401, 'Unauthorized'))
}
//…
}
Após isso, tentaremos decodificar o token com jwt.decode, passando o secret
do config.
try {
const decoded = jwt.decode(token, config.get('jwtTokenSecret'))
const isExpired = moment(decoded.exp).isBefore(new Date())
Se não for possível decodificar, retornaremos um erro:
} catch(err) {
err.status = 401
return next(err)
}
Caso esteja expirado, invocamos o next com um objeto erro, parando a cadeia
de middleware:
const isExpired = moment(decoded.exp).isBefore(new Date())
if(isExpired) {
next(createError(401, 'Unauthorized'))
}
Se estiver tudo certo, podemos colocar o usuário lido do token no objeto
request e invocamos a função next sem nenhum argumento, assim
conseguimos utilizar os dados do usuário do token nos próximos
middlewares:
request.user = decoded.user
next()
Ficando, dessa forma, o middleware verifyJwt:
const verifyJwt = (request, response, next) => {
const token = request.query.token
if (!token) {
return next(createError(401, 'Unauthorized'))
}
try {
const decoded = jwt.decode(token, config.get('jwtTokenSecret'))
const isExpired = moment(decoded.exp).isBefore(new Date())
if(isExpired) {
next(createError(401, 'Unauthorized'))
} else {
request.user = decoded.user
next()
}
} catch(err) {
err.status = 401
return next(err)
}
}
Feito isso, agora basta verificar se o token está válido com um middleware
nas rotas que queremos proteger:
routes.use('/troopers', verifyJwt, trooperRoutes)
Caso a URL /troopers seja acessada sem um token, receberemos o código 401
e a mensagem:
$ curl http://localhost:3000/troopers
{"err":"Unauthorized"}
Ou, com um token inválido, receberemos o código 401 e a mensagem
correspondente:
$ curl http://localhost:3000/troopers?token=a.a.9
{"err":"Unexpected end of JSON input"}
$ curl http://localhost:3000/troopers?token=xpto
{"err":"Not enough or too many segments"}
Apenas se o token for válido
$ curl http://localhost:3000/troopers?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1Ni
J9.eyJ1c2VyIjoicmViZWxzIiwiZXhwIjoxNjEwMjk4MDk4Mzg2fQ.6Hb1TZTyMNxgYdMfBOhv
0AWaGMeQgDsiBl2z075_bwc[{"_id":"5ff1dc16cf0b8e158afa0430","name":"CT-55…]
é que a listagem de soldados irá aparecer.
Dependendo do webserver, a URI tem um limite de caracteres, por isso não é
uma boa prática utilizá-la para enviar o token. Caso quiséssemos enviar
outras informações na querystring, como número da paginação ou algum
filtro, boa parte desse limite já estaria comprometida com o token. Também é
semanticamente mais correto enviar informações extras da requisição no
cabeçalho, por isso iremos alterar o middleware para receber via header o
token.
const token = request.query.token || request.headers['x-token'];
E agora informamos no cabeçalho da requisição:
$ curl http://localhost:3000/troopers -H 'x-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUz
I1NiJ9.eyJ1c2VyIjoicmViZWxzIiwiZXhwIjoxNjEwMjk4MDk4Mzg2fQ.6Hb1TZTyMNxgY
dMfBOhv0AWaGMeQgDsiBl2z075_bwc'
Convém notar que não enviamos no token o usuário e a senha, pois, uma vez
que o token for gerado, não realizamos consultas ao banco de dados, já que,
se o token for válido e não estiver expirado, o usuário e a senha já foram
validados e estão corretos.
A segunda restrição do REST diz que a requisição deve ser stateless e deve
fornecer todos os dados necessários para ser validada, sem que o servidor
precise verificar em outras fontes.
Arquivo server/routes/index.js
import { Router } from 'express'
import trooperRoutes from './trooper.js'
import createError from 'http-errors'
import jwt from 'jwt-simple'
import moment from 'moment'
import config from 'config'
const routes = new Router()
routes.get('/', (req, res) => res.send('Ola s'))
routes.post('/login', (request, response, next) => {
const { username, password } = request.body
if (username === 'rebels' && password === '1138') {
const token = jwt.encode({
user: username,
exp: moment().add(7, 'days').valueOf()
}, config.get('jwtTokenSecret'))
return response.json({ token })
}
next(createError(401, 'Unauthorized'))
})
const verifyJwt = (request, response, next) => {
const token = request.query.token || request.headers['x-token'];
if (!token) {
return next(createError(401, 'Unauthorized'))
}
try {
const decoded = jwt.decode(token, config.get('jwtTokenSecret'))
const isExpired = moment(decoded.exp).isBefore(new Date())
if(isExpired) {
next(createError(401, 'Unauthorized'))
} else {
request.user = decoded.user
next()
}
} catch(err) {
err.status = 401
return next(err)
}
}
routes.use('/troopers', verifyJwt, trooperRoutes)
export default routes
5.7 Fastify
Além do ExpressJS, existem diversos outros frameworks para construção de
APIs em NodeJS. Neste capítulo, veremos brevemente como utilizar o
Fastify. Seguiremos os mesmos conceitos de separação de camadas.
$ mkdir -p src/config src/controller src/hook src/repository
Vamos instalar alguns pacotes:
$ npm i --save fastify mongoist dotenv debug dnscache http-errors
Usaremos o dotenv (https://github.com/motdotla/dotenv) para conter as
configurações da aplicação, assim como a URI do MongoDB. O arquivo .env
na raiz do projeto tem o seguinte conteúdo:
Arquivo .env
MONGO_URI=mongodb://localhost:27017/livro_nodejs
Utilizando o dotenv, acessamos a URI do MongoDB, como variável de
ambiente process.env.MONGO_URI:
Arquivo src/config/mongoist.js
const debug = require('debug')
const mongoist = require('mongoist')
const log = debug('livro_nodejs:config:mongoist')
const db = mongoist(process.env.MONGO_URI)
db.on('error', (err) => log('mongodb err', err))
module.exports = db
No arquivo src/app.js completamos os requisitos para utilizar o dotenv,
colocando no início do arquivo a chamada require('dotenv').config().
Arquivo src/app.js
require('dotenv').config()
O arquivo de repository é exatamente idêntico, só variamos de acordo com o
banco de dados.
Arquivo src/repository/Stormtrooper.js
const mongoist = require('mongoist')
const db = require('../config/mongoist.js')
const Stormtrooper = {
list() {
const query = {}
return db.stormtroopers.find(query)
},
byId(id) {
return db.stormtroopers.findOne({ _id: mongoist.ObjectId(id) })
},
create({ name, nickname, patent, divisions }) {
return db.stormtroopers.insert({ name, nickname, patent, divisions })
},
updateById(id, { name, nickname, patent, divisions }) {
return db.stormtroopers.update({ _id: mongoist.ObjectId(id) }, { $set: { name,
nickname, patent, divisions } })
},
deleteById(id) {
return db.stormtroopers.remove({ _id: mongoist.ObjectId(id) })
},
}
module.exports = Stormtrooper
Declaramos o hook verifyId que valida se o ID tem o formato válido do
MongoDB.
Arquivo src/hook/verifyId.js
const createError = require('http-errors')
const verifyId = (request, reply, done) => {
const id = request.params.id
if (!/^[0-9a-f]{24}$/.test(id)) {
throw createError(422, 'invalid id')
}
done()
}
module.exports = verifyId
Note que a assinatura (request, reply, done) é diferente de um middleware do
express (request, response, next), mas, nesse caso, cumprem o mesmo objetivo.
Arquivo src/app.js
require('dotenv').config()
const controller = require('./controller/Stormtrooper')
const verifyId = require('./hook/verifyId')
const fastify = require('fastify')()
fastify.get('/troopers', controller.list)
fastify.post('/troopers', controller.create)
fastify.get('/troopers/:id', {
onRequest: verifyId,
handler: controller.byId
})
fastify.put('/troopers/:id', {
onRequest: verifyId,
handler: controller.updateById
})
fastify.delete('/troopers/:id', {
onRequest: verifyId,
handler: controller.deleteById
})
module.exports = fastify
O controller e o arquivo de rotas são os dois arquivos mais diferentes entre
Fastify e Express, pois seguem conceitos diferentes, então a nossa
implementação também fica diferente.
Arquivo src/controller/Stormtrooper.js
const repository = require('../repository/Stormtrooper')
const createError = require('http-errors')
const Stormtrooper = {
async list(request, reply) {
const result = await repository.list();
reply.type('application/json').code(200)
return result
},
async byId(request, reply) {
const result = await repository.byId(request.params.id)
if (!result) throw createError(404, 'trooper not found')
reply.type('application/json').code(200)
return result
},
async create(request, reply) {
const result = await repository.create(request.body)
reply.type('application/json').code(201)
return result
},
async updateById(request, reply) {
const result = await repository.updateById(request.params.id, request.body)
reply.type('application/json').code(200)
return result
},
async deleteById(request, reply) {
const result = await repository.deleteById(request.params.id)
reply.type('application/json').code(204)
return ''
}
}
module.exports = Stormtrooper
Seguimos não respondendo erros diretamente nos controllers, mas sim
levantando uma exceção ao criar um objeto Error com a propriedade status.
if (!result) throw createError(404, 'trooper not found')
Apesar de, por enquanto, só haver uma única entidade Stormtrooper, gosto de
já deixar criadas as pastas controller e repository, para numa futura evolução,
onde essa API gerencie outras entidades, já termos uma estrutura sólida e
organizada desde o início.
Já que o listener do servidor ficará no index.js, o package.json para executar o
projeto local fica:
"scripts": {
"dev": "nodemon index.js"
},
Arquivo index.js
const fastify = require('./src/app')
fastify.listen(3000, (err, address) => {
if (err) throw err
fastify.log.info(`server listening on ${address}`)
})
Iremos evoluir o arquivo index.js como fizemos com o server/bin/www.js,
configurando cluster, dnscache, keep alive e posteriormente New Relic, pois
esse é o ponto de entrada da aplicação.
Arquivo index.js
const fastify = require('./src/app')
const dnscache = require('dnscache')
const cluster = require('cluster')
const http = require('http')
const https = require('https')
const cpus = require('os').cpus()
http.globalAgent.keepAlive = true
https.globalAgent.keepAlive = true
dnscache({
enable: true,
ttl: 300,
cachesize: 1000
})
const onWorkerError = (code, signal) => log(code, signal)
if (cluster.isMaster) {
cpus.forEach(_ => {
const worker = cluster.fork()
worker.on('error', onWorkerError);
})
cluster.on('exit', (err) => {
const newWorker = cluster.fork()
newWorker.on('error', onWorkerError)
log('A new worker rises', newWorker.process.pid)
})
cluster.on('exit', (err) => log(err))
} else {
fastify.listen(3000, (err, address) => {
if (err) throw err
fastify.log.info(`server listening on ${address}`)
})
}
Trata-se de uma API, e não importa com qual linguagem ou framework ela
foi desenvolvida, a interface de uso permanece seguindo o padrão REST;
portanto, podemos usar o mesmo Postman ou Insomnia que tínhamos
anteriormente, ou testar com curl no terminal:
$ curl http://localhost:3000/troopers/5ff30c2e7952ec31de6b8e18
$ curl -H 'content-type: application/json' -d '{"name": "CC-1010", "nickname": "Fox",
"patent": "Commander", "divisions": ["501st Legion", "Coruscant Guard"] }'
http://localhost:3000/troopers
$ curl -X DELETE http://localhost:3000/troopers/5ff8adb680347f618f5ee021
5.7.1 Schema
O Fastify possui um conceito de validação que permite verificar se os dados
informados estão no formato esperado e de serialização
(https://www.fastify.io/docs/latest/Validation-and-Serialization/) que permite
ao Fastify compilar a saída com uma função de alta performance. Para isso,
vamos declarar o schema:
Arquivo src/schema/stormtrooper.js
const body = {
type: 'object',
required: ['name', 'patent'],
properties: {
_id: { type: 'string' },
name: { type: 'string' },
nickname: { type: 'string' },
patent: {
type: 'string',
enum: ['General', 'Colonel', 'Commander', 'Major', 'Captain', 'Lieutenant', 'Sergeant',
'Soldier']
},
divisions: {
type: 'array',
items: { type: 'string' }
},
}
}
const query = {}
const params = {
type: 'object',
properties: {
id: { type: 'string' }
}
}
const headers = {}
module.exports = { body, query, params, headers }
E então alterar o arquivo de rotas, declarando a utilização do schema na
entrada e na saída das rotas:
Arquivo src/app.js
require('dotenv').config()
const controller = require('./controller/Stormtrooper')
const verifyId = require('./hook/verifyId')
const schema = require('./schema/stormtrooper')
const fastify = require('fastify')()
fastify.get('/troopers', {
handler: controller.list,
schema: {
response: { 200: { type: 'array', items: schema.body } }
}
})
fastify.post('/troopers', {
schema: {
body: schema.body,
response: { 201: schema.body },
params: schema.params
},
handler: controller.create
})
fastify.get('/troopers/:id', {
schema: {
response: { 200: schema.body },
params: schema.params
},
onRequest: verifyId,
handler: controller.byId
})
fastify.put('/troopers/:id', {
schema: {
body: schema.body,
params: schema.params
},
onRequest: verifyId,
handler: controller.updateById
})
fastify.delete('/troopers/:id', {
schema: {
params: schema.params
},
onRequest: verifyId,
handler: controller.deleteById
})
module.exports = fastify
Com isso, ao tentar criar um soldado sem o campo nome, que é obrigatório,
recebemos um Bad Request:
$ curl -H 'content-type: application/json' -d '{"nickname": "Fox", "patent":
"Commander", "divisions": ["501st Legion", "Coruscant Guard"] }'
http://localhost:3000/troopers
{"statusCode":400,"error":"Bad Request","message":"body should have required
property 'name'"}
Independentemente do framework de rotas que escolhermos, é importante ler
a documentação e aplicar as melhores práticas de desenvolvimento de
software.
5.8 Serverless
O Serverless (https://www.serverless.com) é um framework para
desenvolvimento de funções, como a AWS Lambda
(https://aws.amazon.com/pt/lambda/), Google Cloud Functions
(https://cloud.google.com/functions) e Azure Functions
(https://azure.microsoft.com/en-us/services/functions/). O conceito é colocar
código em produção sem provisionamento e gerenciamento de servidores,
permitindo assim um escalonamento sob demanda do provedor de cloud e
múltiplas formas de integração via eventos (upload de arquivo no S3,
chamada HTTP, mensagem em fila etc.).
O framework Serverless (https://github.com/serverless/serverless) nos ajuda
abstraindo o provedor cloud e provendo uma diversidade grande de plugins
para facilitar o desenvolvimento de funções localmente.
Com o comando a seguir, vamos iniciar o projeto:
$ npx serverless create --template aws-nodejs --path <nome do projeto>
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/wbruno/Sites/wbruno/livro-
nodejs/capitulo_5/5.8"
_______ __
| _ .-----.----.--.--.-----.----| .-----.-----.-----.
| |___| -__| _| | | -__| _| | -__|__ --|__ --|
|____ |_____|__| \___/|_____|__| |__|_____|_____|_____|
| | | The Serverless Application Framework
| | serverless.com, v2.18.0
-------'
serverless.yml
No arquivo serverless.yml colocamos as definições do projeto, como provedor
de cloud que utilizaremos, plugins, qual trigger irá disparar nossa função,
VPC, subnet, criação de domínio, log etc., pois o framework Serverless
cuidará de todo o provisionamento, tendo o aws-cli configurado:
$ serverless deploy -v
handler.js
Com o manipulador do evento recebido, exportamos uma função, recebemos
um objeto event como argumento e devemos retornar um JSON com o status
code e um body como resposta. O código de exemplo gerado de comando
create é o seguinte:
Arquivo handler.js
'use strict';
module.exports.hello = async (event) => {
return {
statusCode: 200,
body: JSON.stringify(
{
message: 'Go Serverless v1.0! Your function executed successfully!',
input: event,
},
null,
2
),
};
// Use this code if you don't use the http event with the LAMBDA-PROXY integration
// return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};
Texto
No arquivo de rota ou no controller, se quisermos responder com um texto,
chamamos o método response.send():
response.send('Patience you must have my young padawan');
JSON
Se quisermos um JSON:
response.json({ "name": "Palpatine", "type": "Sith" });
HTML
Conseguimos enviar arquivos diretamente do NodeJS com o método
response.sendFile:
app.get('/', (request, response) => {
response.sendFile(path.join(__dirname, 'public/index.html'))
})
Para renderizar arquivos .html, interpolando variáveis do backend, depois de
ter configurado algum template engine, utilizaremos o método .render():
response.render('home', {"title": "Página inicial"});
Esse método aceita dois argumentos: o caminho do arquivo de template que
está no diretório views e um objeto JSON com variáveis para serem
injetadas e interpoladas. Para imprimir uma variável injetada pelo método
.render(), utilizamos chaves duplas, a depender da engine, em volta do nome
da variável:
<h1 id="header-title">{{title}}</h1>
No código-fonte renderizado do browser será mostrado:
<h1 id="header-title">Página inicial</h1>
XML
Para responder um XML, como no código a seguir, em que há uma lista de
personagens, temos algumas opções:
<characters>
<character>
<name>Boba Fett</name>
<homeworld>Kamino</homeworld>
</character>
<character>
<name>Jango Fett</name>
<homeworld>Concord Dawn</homeworld>
</character>
<character>
<name>Chewbacca</name>
<homeworld>Kashyyyk</homeworld>
</character>
<characters>
Setar um cabeçalho XML e enviar o conteúdo do XML como texto:
router.get('/xml', (request, response) => {
response.header('Content-Type','text/xml')
response.send('<?xml version="1.0" encoding="UTF-8"?> <characters><character>
<name>Boba Fett</name><homeworld>Kamino</homeworld></character><character>
<name>Jango Fett</name><homeworld>Concord Dawn</homeworld></character>
<character><name>Chewbacca</name><homeworld>Kashyyyk</homeworld>
</character></characters>')
})
Utilizar um mapper objeto-xml, como o node-json2xml
(https://github.com/estheban/node-json2xml) que transforma um JSON em
XML:
const json2xml = require('json2xml')
router.get('/xml-mapper', (request, response) => {
var obj = { "characters": [
{ "character": { "name": "Boba Fett", "homeworld": "Kamino" } },
{ "character": { "name": "Jango Fett", "homeworld": "Concord Dawn" } },
{ "character": { "name": "Chewbacca", "homeworld": "Kashyyyk" } }
]};
response.header('Content-Type','text/xml')
response.send(json2xml(obj))
})
Arquivo server/app.js
import express from 'express'
const app = express()
app.get('/teach', (request, response) => response.send('Always pass on what you have
learned.'))
export default app
Utilizaremos a porta 3001 para essa aplicação de frontend, pois a API
backend está na porta 3000, assim é possível executar as duas aplicações ao
mesmo tempo.
Arquivo server/bin/www.js
#!/usr/bin/env node
import app from '../app.js'
app.listen(3001)
A rota / devolve a string 'Always pass on what you have learned.'. Para testar isso,
digite no seu terminal:
$ npm run dev
e vá até algum navegador no endereço http://localhost:3001/ para ver a frase,
ou faça um curl:
$ curl 'http://localhost:3001/'
Always pass on what you have learned.
Para servir arquivos estáticos com NodeJS, utilizaremos um middleware
built-in do ExpressJS, adicionando a seguinte linha de configuração antes da
definição das rotas no arquivo server/app.js; para CommonJS, temos a variável
global __dirname:
app.use(express.static(path.join(__dirname, 'public')))
Mas em ES6 modules, precisamos simular o __dirname assim:
const __dirname = path.join(dirname(fileURLToPath(import.meta.url)), '../');
app.use(express.static(path.join(__dirname, 'public')))
Arquivo server/app.js
import express from 'express'
import path from 'path'
import { dirname } from 'path'
import { fileURLToPath } from 'url'
const app = express()
const __dirname = path.join(dirname(fileURLToPath(import.meta.url)), '../')
app.get('/', (request, response) => response.send('Always pass on what you have
learned.'))
app.use(express.static(path.join(__dirname, 'public')))
export default app
Arquivo public/style.css
body {
font: 400 16px Helvetica, Arial, sans-serif;
color: #111;
}
Testando:
$ curl 'http://localhost:3001/style.css'
body {
font: 400 16px Helvetica, Arial, sans-serif;
color: #111;
}
O middleware express.static diz que qualquer arquivo na pasta public deve ser
servido diretamente para o cliente, sem nenhum processamento dinâmico, por
isso chamamos de estático. Esse processo fez o arquivo public/style.css estar
acessível.
6.2.1 xhr
Começamos declarando a estrutura no arquivo public/index.html, em que
importamos o public/style.css e o arquivo .js client-side. Além disso, temos uma
tag table#target para receber o retorno da API, já formatado para HTML.
Arquivo public/index.html
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1>Stormtroopers</h1>
<table id="target">
<thead>
<tr>
<th>ID</th><th>Name</th><th>Patent</th>
</tr>
</thead>
<tbody></tbody>
</table>
<script src="/ajax.js"></script>
</body>
</html>
As seguintes alterações no arquivo de estilo apenas para deixar a tabela mais
bonitinha na tela.
Arquivo public/style.css
body {
font: 400 16px Helvetica, Arial, sans-serif;
color: #111;
}
table, th, td {
border: 1px solid #ccc;
border-collapse: collapse;
}
th, td {
padding: 0.4rem;
}
Para utilizar AJAX, o objeto XMLHttpRequest, usaremos o arquivo public/ajax.js.
Arquivo public/ajax.js
((window, document, undefined) => {
const ajax = (url, callback) => {
var xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.addEventListener('load', event => {
callback(null, xhr.response, event)
})
xhr.addEventListener('error', callback)
xhr.send(null)
}
const render = ($target, data) => {
const trs = data.map(item => {
return `<tr>
<td>${item._id}</td>
<td>${item.name}</td>
<td>${item.patent}</td>
</tr>`
})
$target.querySelector('tbody').innerHTML = trs.join('')
}
const $target = document.getElementById('target')
ajax('http://localhost:3000/troopers', (err, result) => {
const data = JSON.parse(result)
render($target, data)
})
})(window, document)
O HTML foi renderizado de forma virtual, conforme mostrado na Figura 6.1,
no Safari.
Ou seja, se visualizarmos o código HTML recebido pelo navegador, teremos
o mesmo conteúdo do index.html, sem os dados:
$ curl 'http://localhost:3001'
…
<table id="target">
<thead>
<tr>
<th>ID</th><th>Name</th><th>Patent</th>
</tr>
</thead>
<tbody></tbody>
</table>
…
6.2.2 fetch
Refatorando para utilizar a nova API fetch (https://developer.mozilla.org/pt-
BR/docs/Web/API/Fetch_API), atualizamos a referência no HTML:
<script src="/fetch.js"></script>
E o código JavaScript fica bem simples, usamos promises:
Arquivo public/fetch.js
((window, document, undefined) => {
const render = ($target, data) => {
const trs = data.map(item => {
return `<tr>
<td>${item._id}</td>
<td>${item.name}</td>
<td>${item.patent}</td>
</tr>`
})
$target.querySelector('tbody').innerHTML = trs.join('')
}
const $target = document.getElementById('target')
fetch('http://localhost:3000/troopers')
.then(response => response.json())
.then(data => render($target, data))
})(window, document)
A função render() é a mesma da anterior.
6.2.3 jQuery
Para usar jQuery, sem necessidade de fazer download, usamos a versão
minificada direto da CDN, já que a versão slim não tem a função $.ajax que
queremos. Para isso, uma pequena alteração HTML:
Arquivo public/index.html
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1>Stormtroopers</h1>
<table id="target">
<thead>
<tr>
<th>ID</th><th>Name</th><th>Patent</th>
</tr>
</thead>
<tbody></tbody>
</table>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
crossorigin="anonymous"></script>
<script src="/script.js"></script>
</body>
</html>
E uma adaptação na função render:
Arquivo public/script.js
(($, window, document, undefined) => {
const render = ($target, data) => {
const trs = data.map(item => {
return `<tr>
<td>${item._id}</td>
<td>${item.name}</td>
<td>${item.patent}</td>
</tr>`
})
$target.find('tbody').html(trs.join(''))
}
const $target = $('#target')
$.ajax({
type: 'GET',
url: 'http://localhost:3000/troopers'
})
.then(data => render($target, data))
})(jQuery, window, document)
O resultado é exatamente o mesmo.
6.2.4 ReactJS
Para consumir a API de stormtroopers com ReactJS (https://reactjs.org),
vamos utilizar o comando create react app (https://github.com/facebook/create-
react-app).
$ npx create-react-app nome_do_projeto
$ cd nome_do_projeto
$ npm start
Se já houver outro processo usando a porta 3000, o comando npm start do
CRA irá nos perguntar se queremos utilizar outra porta; assim, o navegador
irá abrir no endereço http://localhost:3001, o Hello World, mostrado na
Figura 6.2.
6.3.1 Nunjucks
O módulo nunjucks (https://mozilla.github.io/nunjucks/) é um ótimo template
engine, baseado no jinja2. Instale-o e salve-o como dependência do projeto:
$ npm install nunjucks --save
Para configurar, basta importar o módulo e configurar a view engine no
server/app.js.
Arquivo server/app.js
import express from 'express'
import path from 'path'
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import nunjucks from 'nunjucks'
const app = express()
const __dirname = path.join(dirname(fileURLToPath(import.meta.url)), '../')
app.set('view engine', 'html')
app.set('views', path.join(__dirname, 'views'))
nunjucks.configure('views', {
autoescape: true,
express: app,
tags: ''
})
app.get('/', (request, response) => response.send('Always pass on what you have
learned.'))
app.use(express.static(path.join(__dirname, 'public')))
export default app
Dado o arquivo views/index.html
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>{{title}}</h1>
<p>{{message}}</p>
</body>
</html>
A sintaxe do Nunjucks para indicar blocos de conteúdo e includes é {%<type>
<value>%} e para imprimir variáveis é {{<nome da variável>}}.
Renderizaremos o HTML, informando a variável para ser interpolada.
Arquivo server/app.js
import express from 'express'
import path from 'path'
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import nunjucks from 'nunjucks'
const app = express()
const __dirname = path.join(dirname(fileURLToPath(import.meta.url)), '../')
app.set('view engine', 'html')
app.set('views', path.join(__dirname, 'views'))
nunjucks.configure('views', {
autoescape: true,
express: app,
tags: ''
})
app.get('/', (request, response) => {
response.render('index', { title: 'Stormtroopers API', message: 'Always pass on
what you have learned.' })
})
app.use(express.static(path.join(__dirname, 'public')))
export default app
Acessando o navegador, ou conferindo no curl, vemos o resultado:
$ curl http://localhost:3001
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Stormtroopers API</h1>
<p>Always pass on what you have learned.</p>
</body>
</html>
Laço de repetição
Com o template engine, podemos enviar variáveis simples, objetos ou arrays.
routes.get('/loop', (request, response) => {
const movies = [
{ name: 'Episode I: The Phantom Menace', release: 1999 },
{ name: 'Episode II: Attack of the Clones', release: 2002 },
{ name: 'Episode III: Revenge of the Sith', release: 2005 },
{ name: 'Rogue One: A Star Wars Story', release: 2016 },
{ name: 'Episode IV: A New Hope', release: 1977 },
{ name: 'Episode V: The Empire Strikes Back', release: 1980 },
{ name: 'Episode VI: Return of the Jedi', release: 1983 },
{ name: 'Episode VII: The Force Awakens', release: 2015 },
{ name: 'Episode VIII: The Last Jedi', release: 2017 },
{ name: 'Solo: A Star Wars Story', release: 2018 },
{ name: 'Episode IX: The Rise of Skywalker', release: 2019 },
]
response.render('loop', { title: 'Loop page', movies })
})
Nesse caso, no arquivo views/loop.html, precisamos de um loop para iterar
nesse array:
<ul>
{% for movie in movies %}
<li>{{movie.name}} - {{movie.release}}</li>
{% endfor %}
</ul>
O HTML resultante será:
<ul>
<li>Episode I: The Phantom Menace - 1999</li>
<li>Episode II: Attack of the Clones - 2002</li>
<li>Episode III: Revenge of the Sith - 2005</li>
<li>Rogue One: A Star Wars Story - 2016</li>
<li>Episode IV: A New Hope - 1977</li>
<li>Episode V: The Empire Strikes Back - 1980</li>
<li>Episode VI: Return of the Jedi - 1983</li>
<li>Episode VII: The Force Awakens - 2015</li>
<li>Episode VIII: The Last Jedi - 2017</li>
<li>Solo: A Star Wars Story - 2018</li>
<li>Episode IX: The Rise of Skywalker - 2019</li>
</ul>
Controle de fluxo
A maioria dos template engines também é capaz de criar fluxos condicionais,
por exemplo:
app.get('/if', (request, response) => {
response.render('if', { title: 'if', is3D: false })
})
Dado o arquivo views/if.html, irá aparecer o else:
{% if is3D %}
<p>Hell yeah!</p>
{% else %}
<p>=(</p>
{% endif %}
Porém, se is3D fosse true, apareceria Hell yeah!.
6.3.2 Handlebars
O módulo hbs (https://github.com/donpark/hbs) é uma implementação do
Handlebars para NodeJS. Instale-o e salve-o como dependência do projeto:
$ npm install hbs --save
e configure o arquivo server/app.js assim:
Arquivo server/app.js
import express from 'express'
import path from 'path'
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import hbs from 'hbs'
const app = express()
const __dirname = path.join(dirname(fileURLToPath(import.meta.url)), '../')
app.set('view engine', 'html')
app.set('views', path.join(__dirname, 'views'))
app.engine('html', hbs.__express)
app.get('/', (request, response) => {
response.render('index', { title: 'Stormtroopers API', message: 'Always pass on what
you have learned.' })
})
app.get('/loop', (request, response) => {
const movies = [
{ name: 'Episode I: The Phantom Menace', release: 1999 },
{ name: 'Episode II: Attack of the Clones', release: 2002 },
{ name: 'Episode III: Revenge of the Sith', release: 2005 },
{ name: 'Rogue One: A Star Wars Story', release: 2016 },
{ name: 'Episode IV: A New Hope', release: 1977 },
{ name: 'Episode V: The Empire Strikes Back', release: 1980 },
{ name: 'Episode VI: Return of the Jedi', release: 1983 },
{ name: 'Episode VII: The Force Awakens', release: 2015 },
{ name: 'Episode VIII: The Last Jedi', release: 2017 },
{ name: 'Solo: A Star Wars Story', release: 2018 },
{ name: 'Episode IX: The Rise of Skywalker', release: 2019 },
]
response.render('loop', { title: 'Loop page', movies })
})
app.get('/if', (request, response) => {
response.render('if', { title: 'if', is3D: false })
})
app.use(express.static(path.join(__dirname, 'public')))
export default app
Reescrevendo os templates HTML para hbs:
Arquivo views/loop.html
<h1>{{title}}</h1>
<ul>
{{#each movies}}
<li>{{name}} - {{release}}</li>
{{/each}}
</ul>
Arquivo views/if.html
{{#if is3D }}
<p>Hell yeah!</p>
{{ else }}
<p>=(</p>
{{/if}}
6.3.3 Pug
O módulo pug (https://pugjs.org/api/getting-started.html) se chamava Jade
antigamente, porém teve que ser renomeado por questões legais com o nome
Jade registrado por outra empresa (https://github.com/pugjs/pug/issues/2184).
O Jade (https://www.npmjs.com/package/jade) continua sendo o template
engine sugerido pelo express-generator, pois as versões antigas desse pacote
ainda estão disponíveis para instalação. Porém, não é mais mantido,
conforme aviso no npm, mostrado na Figura 6.3.
Arquivos views/if.pug
h1=title
ul
each movie in movies
li= movie.name + ' - ' + movie.release
Não vejo o Pug (ou Jade) muito utilizado ultimamente em grandes projetos.
Arquivo server/app.js
import express from 'express'
import path from 'path'
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import reactViews from 'express-react-views'
const app = express()
const __dirname = path.join(dirname(fileURLToPath(import.meta.url)), '../')
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'jsx')
app.engine('jsx', reactViews.createEngine())
app.get('/', (request, response) => {
response.render('index', { title: 'Stormtroopers API', message: 'Always pass on what
you have learned.' })
})
app.get('/loop', (request, response) => {
const movies = [
{ name: 'Episode I: The Phantom Menace', release: 1999 },
{ name: 'Episode II: Attack of the Clones', release: 2002 },
{ name: 'Episode III: Revenge of the Sith', release: 2005 },
{ name: 'Rogue One: A Star Wars Story', release: 2016 },
{ name: 'Episode IV: A New Hope', release: 1977 },
{ name: 'Episode V: The Empire Strikes Back', release: 1980 },
{ name: 'Episode VI: Return of the Jedi', release: 1983 },
{ name: 'Episode VII: The Force Awakens', release: 2015 },
{ name: 'Episode VIII: The Last Jedi', release: 2017 },
{ name: 'Solo: A Star Wars Story', release: 2018 },
{ name: 'Episode IX: The Rise of Skywalker', release: 2019 },
]
response.render('loop', { title: 'Loop page', movies })
})
app.get('/if', (request, response) => {
response.render('if', { title: 'if', is3D: true })
})
app.use(express.static(path.join(__dirname, 'public')))
export default app
Aqui renomeamos os arquivos .html para .jsx.
Arquivo views/index.jsx
const React = require('react')
function IndexPage(props) {
return (
<>
<html lang="pt-br">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Document</title>
</head>
<body>
<h1>{props.title}</h1>
<p>{props.message}</p>
</body>
</html>
</>
)
}
module.exports = IndexPage
Arquivo views/loop.jsx
const React = require('react')
function LoopPage(props) {
return (
<>
<h1>{props.title}</h1>
<ul>
{
props.movies.map((movie,i) => {
return <li key={i}>{movie.name} - {movie.release}</li>
})
}
</ul>
</>
)
}
module.exports = LoopPage
Arquivo views/if.jsx
const React = require('react')
function IfPage(props) {
return props.is3D ? 'Hell yeah!' : '=('
}
module.exports = IfPage
CAPÍTULO7
Testes automatizados
Imagine ter que simular todos os comportamentos de uma aplicação cada vez
que uma nova linha de código for adicionada. Testar endpoint por endpoint,
com cada uma das possibilidades de dados – corretos e incorretos –,
endpoints que não existem, para verificar 404, simular erros etc. É inviável
fazer isso manualmente a todo momento, não é?
Estamos criando novas funcionalidades, incluindo novas dependências e
refatorando códigos a todo momento. Não queremos que uma alteração em
uma parte do sistema faça com que outra parte pare de funcionar, ou que um
comportamento antigo seja alterado.
Serão os testes automatizados que garantirão a qualidade do software que
estamos entregando. Eles garantirão que um defeito antigo já corrigido não
voltará a aparecer e que um comportamento já testado não irá parar de
funcionar.
AssertionError [ERR_ASSERTION]: 0 == 6
…
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 0,
expected: 6,
operator: '=='
}
O módulo assert disparou uma exceção informando que esperávamos que o
resultado fosse igual a 6, e não igual a 0. Agora podemos implementar a
função. O objetivo é somar os itens, e a primeira coisa que nos vem à mente é
que precisaremos de um laço de repetição.
Arquivo util.js
const arraySum = (arr) => {
let sum = 0;
for(let i = 0, max = arr.length; i < max; i++) {
sum += arr[i];
}
return sum;
}
module.exports = { arraySum }
Agora, ao executar
$ node tests/util.test.js
não aparece nada, pois o teste passou. Entretanto, ainda faltam muitas
situações para serem testadas:
• Testar com outro array.
• E se houver um número negativo no array?
• E se não houver nenhum elemento no array?
• E se houver um zero?
• E se alguma das posições do array não for um número?
Para organizar os casos de testes, utilizaremos um framework de testes.
7.2 Jest
O Jest (https://jestjs.io) é um framework de testes flexível para JavaScript
com suporte para testar códigos assíncronos. Instale-o como dependência de
desenvolvimento no projeto em que você pretende testar os códigos.
$ npm install jest --save-dev
Colocar essa linha shell dentro do scripts no package.json.
"scripts": {
"test": "jest tests/*.test.js"
},
Feito isso, podemos executar com apenas:
$ npm test
Começar com um teste quebrando, depois escrever um código que faz o teste
passar, é um dos princípios do TDD. Mais à frente, vamos refatorar o código
para fazer melhorias nele.
Podemos usar o método describe(), para agrupar um conjunto de testes, ou
apenas escrever teste a teste, cada um em um it.
Arquivo tests/util.test.js
const assert = require('assert')
const util = require('../util')
it('should sum the array [1,2,3]', () => {
const sum = util.arraySum([1,2,3])
assert.equal(sum, 6)
})
Ao executar o npm test:
$ npm test
> [email protected] test /Users/wbruno/Sites/wbruno/livro/capitulo_7/7.1
> jest tests/*.test.js
PASS tests/util.test.js
ü should sum the array [1,2,3] (1 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.808 s, estimated 1 s
Ran all test suites matching /tests\/util.test.js/i.
E então incluir os outros casos de testes:
const assert = require('assert')
const util = require('../util')
it('should sum the array [1,2,3]', () => {
const sum = util.arraySum([1,2,3])
assert.equal(sum, 6)
})
it('should sum the array [1,5,6,30]', () => {
var sum = util.arraySum([1,5,6,30])
assert.equal(sum, 42)
})
it('should sum the array [7,0,0,0]', () => {
var sum = util.arraySum([7,0,0,0])
assert.equal(sum, 7)
})
it('should sum the array [-1,-2]', () => {
var sum = util.arraySum([-1,-2])
assert.equal(sum, -3)
})
it('should sum the array [0,undefined]', () => {
var sum = util.arraySum([0,undefined])
assert.equal(sum, 0)
})
Ao executar toda a suíte de testes pelo terminal, veremos quais passam e
quais falham:
$ npm test
> [email protected] test /Users/wbruno/Sites/wbruno/livro/capitulo_7/7.1
> jest tests/*.test.js
FAIL tests/util.test.js
ü should sum the array [1,2,3] (1 ms)
ü should sum the array [1,5,6,30]
ü should sum the array [7,0,0,0]
ü should sum the array [-1,-2] (1 ms)
ü should sum the array [0,undefined] (1 ms)
ü should sum the array [0,undefined]
assert.equal(received, expected)
Expected value to be equal to:
0
Received:
NaN
20 | it('should sum the array [0,undefined]', () => {
21 | var sum = util.arraySum([0,undefined])
> 22 | assert.equal(sum, 0)
| ^
23 | })
24 |
at Object.<anonymous> (tests/util.test.js:22:10)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 4 passed, 5 total
Snapshots: 0 total
Time: 0.87 s, estimated 1 s
Ran all test suites matching /tests\/util.test.js/i.
npm ERR! Test failed. See above for more details.
Repare que a frase que passamos como primeiro argumento da função it() é a
descrição do caso de teste, e é ela que aparece no resultado da execução para
saber qual teste passou e qual falhou. Quando criar os seus testes, procure
descrevê-los bem.
Agora que já temos uma cobertura de testes bacana, podemos refatorar o
código:
const arraySum = (arr) => {
return arr.reduce((prev, curr) => prev + curr)
}
module.exports = { arraySum }
e tratar o caso do undefined ou outra coisa que não seja um número:
const arraySum = (arr) => {
return arr
.filter(item => !isNaN(item))
.reduce((prev, curr) => prev + curr)
}
module.exports = { arraySum }
Agora todos os casos de testes estão verdes, garantindo que a nossa mudança
no código não comprometeu o comportamento desejado.
$ npm test
> [email protected] test /Users/wbruno/Sites/wbruno/livro/capitulo_7/7.1
> jest tests/*.test.js
PASS tests/util.test.js
ü should sum the array [1,2,3] (1 ms)
ü should sum the array [1,5,6,30]
ü should sum the array [7,0,0,0]
ü should sum the array [-1,-2] (1 ms)
ü should sum the array [0,undefined]
Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
Snapshots: 0 total
Time: 0.856 s, estimated 1 s
Ran all test suites matching /tests\/util.test.js/i.
O Jest possui o método it.each() para esses casos de teste em que variamos
valores de entrada e saída, mas o corpo é o mesmo, evitando duplicação de
código do teste.
const assert = require('assert')
const util = require('../util')
const cases = [
{ expected: 6, arr: [1,2,3] },
{ expected: 42, arr: [1,5,6,30] },
{ expected: 7, arr: [7,0,0,0] },
{ expected: -3, arr: [-1,-2] },
{ expected: 0, arr: [0,undefined] },
]
it.each(cases)('should sum the array %j', (test) => {
const sum = util.arraySum(test.arr)
assert.equal(sum, test.expected)
})
Quando incluirmos mais uma função no módulo util:
const arraySum = (arr) => {
return arr
.filter(item => !isNaN(item))
.reduce((prev, curr) => prev + curr)
}
const guid = () => {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1)
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4()
}
module.exports = { arraySum, guid }
criaremos também uma nova suíte de testes, agrupando cada conjunto com
describe():
const assert = require('assert')
const util = require('../util')
const cases = [
{ expected: 6, arr: [1,2,3] },
{ expected: 42, arr: [1,5,6,30] },
{ expected: 7, arr: [7,0,0,0] },
{ expected: -3, arr: [-1,-2] },
{ expected: 0, arr: [0,undefined] },
]
describe('#arraySum', () => {
it.each(cases)('should sum the array %j', (test) => {
const sum = util.arraySum(test.arr)
assert.equal(sum, test.expected)
})
})
describe('#guid', () => {
it('should have a valid format', () => {
var uuid = util.guid()
console.log(uuid)
assert.ok(/^[a-z|\d]{8}-[a-z|\d]{4}-[a-z|\d]{4}-[a-z|\d]{4}-[a-z|\d]{12}$/.test(uuid))
})
it('should generate uniques uuids', () => {
var uuid1 = util.guid()
var uuid2 = util.guid()
var uuid3 = util.guid()
var uuid4 = util.guid()
assert.notEqual(uuid1, uuid2)
assert.notEqual(uuid2, uuid3)
assert.notEqual(uuid3, uuid4)
assert.notEqual(uuid1, uuid4)
})
})
Ao executar, temos:
$ npm test
> [email protected] test /Users/wbruno/Sites/wbruno/livro/capitulo_7/7.1
> jest tests/*.test.js
PASS tests/util.test.js
#arraySum
ü should sum the array {"expected":6,"arr":[1,2,3]} (1 ms)
ü should sum the array {"expected":42,"arr":[1,5,6,30]}
ü should sum the array {"expected":7,"arr":[7,0,0,0]}
ü should sum the array {"expected":-3,"arr":[-1,-2]}
ü should sum the array {"expected":0,"arr":[0,null]}
#guid
ü should have a valid format (13 ms)
ü should generate uniques uuids
console.log
fdf3988b-6c89-9bea-f672-e37aa2263a03
at Object.<anonymous> (tests/util.test.js:20:13)
Test Suites: 1 passed, 1 total
Tests: 7 passed, 7 total
Snapshots: 0 total
Time: 0.944 s, estimated 1 s
Ran all test suites matching /tests\/util.test.js/i.
Veja que existe uma string do uuid no meio do relatório. Ela apareceu ali por
causa do console.log() que chamei no método it().
O módulo istanbul (https://github.com/gotwarlost/istanbul) é uma ferramenta
que gera relatórios de cobertura de código com base nos testes que foram
executados. E o Jest já possui o istanbul integrado, é só informar a flag --
coverage.
"scripts": {
"test": "jest tests/*.test.js --coverage"
},
Ao executar, a cobertura de declarações, ramificações, funções e linhas é
impressa:
$ npm test
…
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
util.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 7 passed, 7 total
Snapshots: 0 total
Time: 1.074 s
Ran all test suites matching /tests\/util.test.js/i.
7.2.1 beforeAll,afterAll,beforeEach,afterEach
Antes de iniciar um teste, às vezes precisamos preparar alguma coisa para
que o código execute, como criar um HTML falso para o teste, simular um
objeto, conectar em um banco de dados, limpar uma tabela ou apagar algum
dado da sessão, por exemplo. O método beforeAll() é executado antes da suíte
de teste.
De forma semelhante, o método afterAll() é executado após o último teste da
suíte, geralmente para desfazer algo que a suíte tenha modificado e possa
interferir na próxima bateria de testes.
Por definição, um teste deve ser executado independentemente do resultado
do teste que foi executado antes dele, então um caso de teste não pode
interferir em outro, por isso temos os hooks beforeEach() e afterEach para que
possamos reiniciar o valor de uma variável, limpar uma tabela do banco,
apagar um arquivo etc.
describe('hooks', () => {
beforeAll(() => {
// runs before all tests in this block
})
afterAll(() => {
// runs after all tests in this block
})
beforeEach(() => {
// runs before each test in this block
})
afterEach(() => {
// runs after each test in this block
})
// test cases
})
7.2.2 ESlint
Isso pronto, podemos criar mais uma função no nosso módulo util.js, fora do
padrão do restante do código.
const isBiggerThan = (arr, minValue) => {
let biggest = [];
for(let i = 0, max = arr.length; i < max; i++) {
if (arr[i] >= minValue) {
biggest.push(arr[i]);
}
}
return biggest;
};
E configurar o ESlint:
$ npm i --save-dev eslint
$ npx eslint --init
ü How would you like to use ESLint? · style
ü What type of modules does your project use? · commonjs
ü Which framework does your project use? · none
ü Does your project use TypeScript? · No / Yes
ü Where does your code run? · node
ü How would you like to define a style for your project? · prompt
ü What format do you want your config file to be in? · JSON
ü What style of indentation do you use? · 2
ü What quotes do you use for strings? · single
ü What line endings do you use? · unix
ü Do you require semicolons? · No / Yes
Local ESLint installation not found.
The config that you've selected requires the following dependencies:
eslint@latest
…
Com essas respostas, foi criado o seguinte arquivo .eslintrc.json:
$ cat .eslintrc.json
{
"env": {
"commonjs": true,
"es2021": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 12
},
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"never"
]
}
}
E agora podemos adicionar o pretest:
{
"name": "7.1",
"version": "1.0.0",
"description": "",
"main": "util.js",
"directories": {
"test": "tests"
},
"scripts": {
"pretest": "eslint --fix util.js",
"test": "jest tests/*.test.js --coverage"
},
"keywords": [],
"author": "William Bruno <[email protected]> (http://wbruno.com.br)",
"license": "ISC",
"devDependencies": {
"eslint": "7.17.0",
"jest": "26.6.3"
},
"dependencies": {}
}
Ao executar o npm test, o pretest também será executado:
$ npm test
> [email protected] pretest /Users/wbruno/Sites/wbruno/livro/capitulo_7/7.2
> eslint --fix util.js
> [email protected] test /Users/wbruno/Sites/wbruno/livro/capitulo_7/7.2
> jest tests/*.test.js --coverage
PASS tests/util.test.js
…
Então o arquivo util.js será corrigido com as regras que podemos customizar
junto à equipe.
Para não perder o costume, vamos escrever alguns casos de teste para essa
nova função:
describe('isBiggerThan', () => {
it('should return [3,4,5] from input [1,2,3,4,5], 3', () => {
assert.deepEqual(util.isBiggerThan([1,2,3,4,5], 3), [3,4,5])
})
it('should return [] from input [1,2,3,4,5], 10', () => {
assert.deepEqual(util.isBiggerThan([1,2,3,4,5], 10), [])
})
})
Agora que os testes passaram, podemos refatorar novamente para a nossa
versão final, utilizando o poder funcional do JavaScript.
const isBiggerThan = (arr, minValue) => arr.filter(item => item >= minValue)
A função Array.prototype.filter nos proporciona um código mais claro.
8.1 Healthcheck
Uma boa aplicação web disponibiliza alguma forma que indica se há algum
problema com ela mesma ou com as suas dependências, facilitando assim o
diagnóstico em caso de falha. Para isso, vamos criar alguns novos endpoints
que ajudarão a monitorar a aplicação.
Registramos a nova rota /checks no server/routes/index.js.
Arquivo server/routes/index.js
import express from 'express'
import trooperRoutes from './trooper.js'
import checkRoutes from './check.js'
const routes = new express.Router()
routes.get('/', (req, res) => {
res.send('Ola s')
})
routes.use('/troopers', trooperRoutes)
routes.use('/checks', checkRoutes)
export default routes
E criaremos três endpoints: /version, /status e /status/complete.
8.1.1 /check/version
Retornará a versão da aplicação, ajudando os clientes da API a identificarem
se são compatíveis com a versão que está no ar, ou se um deploy atualizou
corretamente a versão em todas as máquinas, por exemplo.
Ao acessar a rota http://localhost:3000/check/version, veremos o nome da
aplicação e o número da versão, que foram lidos diretamente do arquivo
package.json.
{
"applicationName": "livro",
"versionRelease": "1.0.0",
"uptime": 1.246098212,
"nodeVersion": "v15.5.0
}
Arquivo server/routes/check.js
import express from 'express'
import fs from 'fs/promises'
import path, { dirname } from 'path'
import { fileURLToPath } from 'url'
const routes = new express.Router()
routes.get('/version', async (request, response) => {
const __dirname = dirname(fileURLToPath(import.meta.url))
const str = await fs.readFile(path.join(__dirname, '../../package.json'))
const pkg = JSON.parse(str.toString())
response.json({
applicationName: pkg.name,
versionRelease: pkg.version,
uptime: process.uptime(),
nodeVersion: process.version
})
})
export default routes
8.1.2 /check/status
Essa é uma URL para ping que responderá rapidamente uma mensagem de
sucesso.
Arquivo server/routes/check.js
import express from 'express'
const routes = new express.Router()
routes.get('/status', (request, response) => response.end('PONG'))
export default routes
Esperamos apenas um texto qualquer e um status code 200 como resultado.
Geralmente utilizamos esse tipo de endpoint para check do load balancer,
smoke testes etc.
8.1.3 /check/status/complete
Nessa URL testaremos todas as dependências externas, como conexões com
bancos de dados, web services de terceiros, servidores de mensageria etc.
Queremos que o retorno da rota /check/status/complete nos diga quais
dependências a aplicação tem e como cada uma delas está:
{
"ok": true,
"checks": [
{
"name": "mongo",
"ok": true,
"db": "livro_nodejs"
},
{
"name": "postgres",
"ok": true
},
{
"name": "redis",
"ok": true
}
]
}
Simulando o Postgres, MongoDB e Redis com problemas, parando cada
serviço no OS X:
$ brew services stop postgres
Stopping `postgresql`... (might take a while)
==> Successfully stopped `postgresql` (label: homebrew.mxcl.postgresql)
Queremos um retorno desse endpoint que nos diga, de forma rápida, qual
dependência externa está com falhas e uma mensagem curta do motivo, algo
como:
{
"ok": false,
"checks": [
{
"name": "mongo",
"ok": false,
"message": "connect ECONNREFUSED 127.0.0.1:27017"
},
{
"name": "postgres",
"ok": false,
"message": "connect ECONNREFUSED 127.0.0.1:5432"
},
{
"name": "redis",
"ok": false,
"message": "Redis connection to localhost:6379 failed - connect ECONNREFUSED
127.0.0.1:6379"
}
]
}
Para isso, vou modificar o arquivo de conexão de cada banco de dados,
incluindo uma função .check(), que nos dirá algo sobre a saúde de cada
dependência. Assim, podemos, no endpoint /status/complete, acessar cada uma
das funções: mongo.check(), pg.check() e redis.check() caso essa aplicação tenha
essas três dependências.
Arquivo server/routes/check.js
import express from 'express'
import mongo from '../config/mongoist.js'
import pg from '../config/pg.js'
import redis from '../config/redis.js'
const routes = new express.Router()
routes.get('/status/complete', async (request, response, next) => {
const checks = [await mongo.check(), await pg.check(), await redis.check()]
const ret = {
ok: checks.every(item => item.ok),
checks,
}
response.json(ret)
})
export default routes
Em cada dependência, vamos rodar um comando simples que deve sempre
retornar um valor, se estiver saudável, ou um erro, caso tenha alguma falha. É
importante escolher algo que não dependa de um dado específico, ou a
existência de uma tabela, como um select version(), db.stats().
Arquivo server/config/mongoist.js
import debug from 'debug'
import mongoist from 'mongoist'
import config from 'config'
const log = debug('livro_nodejs:config:mongoist')
const db = mongoist(config.get('mongo.uri'))
db.on('error', (err) => log('mongodb err', err))
db.check = async () => {
let result = { name: 'mongo' }
try {
const data = await db.stats()
result.ok = data.ok === 1
result.db = data.db
} catch (e) {
result.ok = false
result.message = e.message
}
return {
name: 'mongo',
...result
}
}
export default db
Para testar o MongoDB, escolhi usar a chamada db.stats():
> db.stats()
{
"db" : "livro_nodejs",
"collections" : 1,
"views" : 0,
"objects" : 1,
"avgObjSize" : 38,
"dataSize" : 38,
"storageSize" : 20480,
"indexes" : 1,
"indexSize" : 20480,
"totalSize" : 40960,
"scaleFactor" : 1,
"fsUsedSize" : 268539449344,
"fsTotalSize" : 1000240963584,
"ok" : 1
}
que retorna informações sobre o database, como nome, quantidade de
collections, índices e espaço utilizado. Para o Postgres, escolhi usar um select
version() que retorna a versão da engine do Postgres instalado:
livro_nodejs=# select version();
version
------------------------------------------------------------------------------
PostgreSQL 13.1 on x86_64-apple-darwin19.6.0, compiled by Apple clang version
12.0.0 (clang-1200.0.32.27), 64-bit
(1 row)
Arquivo server/config/pg.js
import pg from 'pg'
import debug from 'debug'
const log = debug('livro_nodejs:config:pg')
const pool = new pg.Pool({
user: 'wbruno',
password: '',
host: 'localhost',
port: 5432,
database: 'livro_nodejs',
max: 5
})
pool.on('error', (err) => log('postgres err', err))
pool.check = async () => {
let result = {}
try {
const data = await pool.query('select version()')
result.ok = !!data.rows[0].version
} catch (e) {
result.ok = false
result.message = e.message
}
return {
name: 'postgres',
...result
}
}
export default pool
Para o Redis, resolvi usar os eventos error e connect para guardar o estado da
conexão em uma variável de escopo mais alto, pois, ao disparar algum
evento, o estado dessa variável é alterado.
Arquivo server/config/redis.js
import redis from 'redis'
import { promisify } from 'util'
const client = redis.createClient({
host: 'localhost',
port: 6379
})
const getAsync = promisify(client.get).bind(client)
const setAsync = promisify(client.set).bind(client)
let result
client.on('error', (err) => {
result = { ok: false, message: err.message }
})
client.on('connect', () => {
result = { ok: true }
})
const check = async () => {
return {
name: 'redis',
...result
}
}
export default { getAsync, setAsync, check }
Com a ajuda do Healthcheck, somos capazes de identificar, rapidamente,
problemas, como falta de ACL, bloqueio por firewall, qual dependência
parou de responder etc.
8.3 Logs
Logs são registros de eventos que aconteceram, dados uma certa situação e
um determinado período. Podem nos ajudar a decifrar algum bug, entender o
motivo de uma requisição não ter tido o efeito esperado, por exemplo, e até
nos poupar horas de trabalho, se construirmos alertas e gráficos baseados
neles.
Pacotes como o Morgan (https://github.com/expressjs/morgan) ou o Winston
(https://github.com/winstonjs/winston) podem nos ajudar a escrever logs e
exportá-los para algum servidor centralizador, como um Splunk ou Graylog.
Mas o que é importante saber sobre logs é entender o que de fato logar ou
não. Um log mínimo deve sempre responder pelo menos essas três perguntas:
Quando? Quem? O quê?
Logo, é importante ter informações, como horário e origem, endpoint e
método HTTP utilizado, IP ou detalhes sobre o usuário que fez a requisição,
como email ou clientId, e o que aquilo representa no sistema.
Assim como podemos inserir diversos níveis de log, info, warning, error,
dependendo da criticidade da operação, às vezes é necessário ter diversos
logs em um mesmo request, para conseguir fazer o acompanhamento de até
que ponto uma certa informação foi processada.
8.4 forever e pm2
O módulo forever (https://github.com/foreverjs/forever) permite que uma
aplicação NodeJS fique em execução contínua, pois faz o restart do processo
caso alguma falha na aplicação cause um kill. Enquanto usamos o nodemon
em desenvolvimento, usamos o forever em produção.
Nós o instalaremos globalmente:
$ npm install forever -g
E iniciaremos a aplicação no servidor, com o comando:
$ forever start /var/www/site.com.br/bin/www
Feito isso, alguns dos métodos que temos disponíveis são: start, stop, stopAll,
list, restart e restartAll.
$ forever stop /var/www/site.com.br/bin/www
$ forever list
$ forever restart /var/www/site.com.br/bin/www
Esse módulo ficará responsável por reiniciar o processo NodeJS caso alguma
falha faça com que ele pare, diminuindo assim o tempo que a aplicação fica
sem responder.
O pm2 (https://github.com/Unitech/pm2) traz a mesma ideia do forever:
$ npm install pm2 -g
$ pm2 start /var/www/site.com.br/bin/www
8.5 Nginx
Não deixamos que o NodeJS sirva diretamente na porta 80 por motivos de
segurança, já que, para um processo ser executado com listener em uma porta
abaixo de 1024, é necessário um nível alto de permissão na máquina.
Por esse motivo, utilizamos os números de porta sempre acima de 1024. Não
queremos que o NodeJS seja executado com permissões de root, já que
atacantes estão sempre procurando formas de fazer com que o servidor
execute comandos por eles. Ao colocar o NodeJS atrás do Nginx,
estabelecemos uma primeira camada de segurança, como vemos na Figura
8.1.
Figura 8.1 – Nginx como proxy reverso.
O Nginx (http://nginx.org/en/) é o servidor web para alta concorrência,
performance e baixo uso de memória. Ele trabalha de uma forma muito
semelhante ao NodeJS e foi, inclusive, a inspiração para Ryan Dahl, ao
utilizar uma arquitetura assíncrona baseada em eventos para lidar com as
requisições. Ele fará um proxy da porta 3000 para a porta 80, que é aquela
que fica aberta para aplicações web HTTP por padrão.
Além disso, a configuração de HTTPS (porta 443), o cache de estáticos,
cabeçalhos de segurança, brotli ou gzip, o roteamento de subdomínio e o
bloqueio de DDoS podem ficar a cargo do proxy, e não da aplicação em si;
dessa forma, as threads do NodeJS ficam liberadas para lidar com o que
realmente é dinâmico.
Arquivo nginx.conf
events {
worker_connections 4096;
}
http {
upstream nodejs {
server localhost:3000;
}
server {
listen 80;
server_name localhost;
access_log access.log;
error_log error.log;
location / {
proxy_pass http://nodejs;
}
}
}
Para iniciar o Nginx local, usamos o comando:
$ nginx -c $(pwd)/nginx.conf -p $(pwd)
Crie um arquivo public/50x.html na aplicação para ser um HTML estático que o
Nginx irá servir, caso a aplicação não responda.
Arquivo 50x.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Internal error</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
body,html { font-size: 100%; height: 100%; }
body { font-family: Arial,sans-serif; font-weight: 400; font-style: normal; }
h1 { color: rgb(153, 153, 153); text-align: center; }
h1 strong { display: block; font-size: 100px; font-weight: 400; }
h1 span { font-size: 16px; font-weight: 400; }
</style>
</head>
<body>
<h1><strong>Ops!</strong><span>Internal error.</span></h1>
</body>
</html>
Idealmente, esse arquivo deve ser o mais leve possível, por isso use imagens
com cuidado e tente inserir todo o conteúdo de folhas de estilo CSS no
próprio arquivo .html. Uma boa técnica a ser utilizada é codificar as imagens
com base64.
Assim, podemos configurar para servir em caso de falha no upstream:
root /Users/wbruno/Sites/wbruno/livro/capitulo_8/8.1/public/;
error_page 404 500 502 503 504 /50x.html;
location /50x.html {
internal;
}
Agora que teremos o Nginx na frente da aplicação NodeJS, podemos
transferir o trabalho de servir arquivos estáticos para ele. Altere a declaração
do middleware express.static(), linha no arquivo server/app.js, para só ser
executado se estivermos em ambiente de desenvolvimento:
if (app.get('env') === 'development') {
app.use(express.static(path.join(__dirname, 'public')));
}
E adicione, no arquivo de configuração do Nginx, um location para servir
arquivos estáticos:
location ~* \.(?:ico|css|html|json |js|map|gif|jpe?g|png|ttf|woff|woff2|svg|eot|txt|csv)$ {
ss|html|json|js|map|gif|jpe?g|png|ttf|woff|woff2|svg|eot|txt|csv)$ {
access_log off;
expires 30d;
add_header pragma public;
add_header Cache-Control "public, mustrevalidate, proxy-revalidate";
}
Dessa forma, utilizaremos o NodeJS para servir arquivos da pasta public
apenas no ambiente de desenvolvimento, enquanto no servidor o Nginx se
encarregará de servir em produção.
8.5.1 compression
O compression (https://github.com/expressjs/compression) faz o trabalho de
diminuir a resposta, retornando um binário menor e otimizado, em vez de
texto puro; em alguns casos, a diminuição chega a 70%, diminuindo o tempo
de download e economizando tráfego; logo, deixando a requisição mais
rápida. Instale:
$ npm install helmet --save
E invoque o middleware, antes de qualquer rota:
const compression = require('compression')
const app = express()
app.use(compression({ threshold : 0 }))
Também podemos fazer isso no proxy reverso, configurando algumas
diretivas dentro do HTTP.
Lembrando que brotli oferece um nível de compressão maior e é mais rápido
que gzip, mas será necessário instalar um módulo extra:
brotli on;
brotli_comp_level 4;
brotli_types text/html text/plain text/css application/javascript application/json
image/svg+xml application/xml+rss;
brotli_static on;
E para gzip:
gzip on;
gzip_disable "msie6";
gzip_min_length 1;
gzip_types *;
gzip_http_version 1.1;
gzip_vary on;
gzip_comp_level 6;
gzip_proxied any;
gzip_buffers 16 8k;
Caso tenha ativado o brotli, mantenha o gzip também, pois, se algum cliente
não suportar brotli, ele receberá pelo menos gzipado.
$ curl --head http://localhost:80/ -H 'Accept-Encoding: br, gzip'
HTTP/1.1 200 OK
…
Content-Encoding: gzip
Veremos o Content-Encoding como resposta se tudo estiver configurado
corretamente.
8.5.2 Helmet
O Helmet (https://github.com/helmetjs/helmet) é um pacote que ajuda na
segurança da aplicação, colocando alguns cabeçalhos. Uma boa prática, por
exemplo, é remover o X-Powered-By: Express, pois indica sem necessidade
nenhuma com qual framework a aplicação foi desenvolvida, e isso pode
facilitar ataques direcionados ao ExpressJS.
Instale:
$ npm install helmet --save
E declare como middleware, antes de qualquer rota, assim todas as respostas
após o Helmet estarão com os cabeçalhos.
const express = require('express')
const helmet = require('helmet')
const app = express()
app.disable('x-powered-by')
app.use(helmet())
app.get('/', (request, response) => response.send(''))
app.listen(3000)
Na invocação padrão, o Helmet já faz todos os cabeçalhos a seguir:
app.use(helmet.contentSecurityPolicy())
app.use(helmet.dnsPrefetchControl())
app.use(helmet.expectCt())
app.use(helmet.frameguard())
app.use(helmet.hidePoweredBy())
app.use(helmet.hsts())
app.use(helmet.ieNoOpen())
app.use(helmet.noSniff())
app.use(helmet.permittedCrossDomainPolicies())
app.use(helmet.referrerPolicy())
app.use(helmet.xssFilter())
Fica assim o resultado dos requests após a instalação do Helmet:
$ curl -i http://localhost:3000/
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self';base-uri 'self';block-all-mixed-content;font-src
'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src
'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests
X-DNS-Prefetch-Control: off
Expect-CT: max-age=0
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Download-Options: noopen
X-Content-Type-Options: nosniff
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: no-referrer
X-XSS-Protection: 0
Content-Type: text/html; charset=utf-8
Content-Length: 0
ETag: W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"
Date: Thu, 07 Jan 2021 12:18:44 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Porém, tendo o Nginx na frente da aplicação NodeJS, eu também prefiro
fazer esse tipo de trabalho no proxy reverso:
Arquivo nginx.conf
events {
worker_connections 4096;
}
http {
upstream nodejs {
server localhost:3000;
}
server_tokens off;
charset utf-8;
# brotli on;
# brotli_comp_level 4;
# brotli_types text/html text/plain text/css application/javascript application/json
image/svg+xml application/xml+rss;
# brotli_static on;
gzip on;
gzip_disable "msie6";
gzip_min_length 1;
gzip_types *;
gzip_http_version 1.1;
gzip_vary on;
gzip_comp_level 6;
gzip_proxied any;
gzip_buffers 16 8k;
server {
listen 80;
server_name localhost;
access_log access.log;
error_log error.log;
location / {
add_header content-security-policy "default-src 'self';base-uri 'self';block-all-
mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self'
data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https:
'unsafe-inline';upgrade-insecure-requests";
add_header x-content-security-policy "default-src 'self';base-uri 'self';block-all-
mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self'
data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https:
'unsafe-inline';upgrade-insecure-requests";
add_header x-webkit-csp "default-src 'self';base-uri 'self';block-all-mixed-
content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-
src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-
inline';upgrade-insecure-requests";
add_header x-dns-prefetch-control off;
add_header expect-ct "max-age=0";
add_header x-frame-options SAMEORIGIN;
add_header strict-transport-security "max-age=15552000;
includeSubdomains";
add_header x-download-options noopen;
add_header x-content-type-options nosniff;
add_header x-permitted-cross-domain-policies none;
add_header referrer-policy no-referrer;
add_header x-xss-protection "1; mode=block";
proxy_pass http://nodejs;
}
}
}
Para subir o Nginx localmente:
$ nginx -c $(pwd)/nginx.conf -p $(pwd)
Para testar diversas configurações e matar o Nginx, estou usando kill <id do
processo>, e um ps aux para descobrir o pid:
$ ps aux | grep nginx
wbruno 45317 … 0:00.00 grep --color=auto nginx
wbruno 30458 … 0:00.00 nginx: worker process
wbruno 30457 … 0:00.00 nginx: master process nginx -c …nginx.conf -p …
$ kill 30457
8.6 Docker
O uso de Docker é muito comum hoje em dia; por isso, temos até exemplo na
documentação oficial do NodeJS (https://nodejs.org/en/docs/guides/nodejs-
docker-webapp/). A ideia do Docker (https://www.docker.com) é criar uma
imagem com tudo o que a aplicação precisa para executar. Para isso, tendo o
Docker instalado localmente, vamos criar o arquivo Dockerfile na raiz da
aplicação:
Arquivo Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm ci --only=production
# Bundle app source
COPY . .
EXPOSE 3000
CMD [ "node", "server/bin/www.js" ]
O arquivo .dockerignore diz para o Docker quais arquivos locais ele pode
ignorar durante o processo de build da imagem. Adicionamos a pasta
node_modules, pois faremos o npm install dentro da imagem novamente, já que
algumas dependências podem precisar ser compiladas no sistema operacional
específico (algumas têm partes do código em C), e também não vamos
instalar as dependências de desenvolvimento.
Arquivo .dockerignore
node_modules
*.log
Execute o comando docker build, na raiz do projeto, para construir a imagem
Docker:
$ docker build -t wbruno/livro_nodejs .
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
wbruno/livro_nodejs latest feb5a2ac20b8 23 seconds ago 1GB
node 14 cb544c4472e9 24 hours ago 942MB
E assim podemos executar, expondo localmente na porta 8080, o que está na
porta 3000 do Docker, pois foi na 3000 que colocamos o server.listen().
$ docker run -p 8080:3000 -d wbruno/livro_nodejs
Para conferir quais contêineres estão em execução, usamos docker ps:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
003621e4c6b1 wbruno/livro_nodejs "docker-entrypoint.s…" 57 seconds ago Up 56
seconds 8080/tcp, 0.0.0.0:8080->3000/tcp pedantic_antonelli
E, para matar um contêiner, basta copiar o contêiner ID do docker ps e
informar no docker kill:
$ docker kill 003621e4c6b1
003621e4c6b1
Para otimizar, vamos alterar a imagem base, para usar alpine
(https://hub.docker.com/_/node), modificando o arquivo Dockerfile:
FROM node:14-alpine
E após construir novamente:
$ docker build -t wbruno/livro_nodejs-alpine .
Vemos que a imagem gerada é muito menor, de 1GB para 174MB.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
wbruno/livro_nodejs-alpine latest ab2349e389a9 57 seconds ago 174MB
wbruno/livro_nodejs latest feb5a2ac20b8 10 minutes ago 1GB
Para conectar a aplicação que está dentro do Docker ao MongoDB que está
na máquina local, é necessário editar a string de conexão antes de gerar a
imagem da aplicação trocando localhost para host.docker.internal.
Arquivo config/default.json
{
"mongo": {
"uri": "mongodb://host.docker.internal:27017/livro_nodejs"
}
}
8.8 AWS
Com a AWS como IaaS (Infrastructure as a Service), ao utilizar o serviço
EC2, contratamos uma máquina limpa, sem nada instalado, apenas a
distribuição Linux que escolhemos. Após criar uma conta no console
(https://aws.amazon.com/pt/console/), dentro do painel do serviço EC2, vá
em Key Pairs (menu da esquerda), conforme a Figura 8.4:
Figura 8.4 – Painel EC2 da AWS, onde criaremos uma nova key pair.
Uma key pair é a chave SSH que usaremos para acessar as instâncias EC2
(Figura 8.5).
Depois de fazer download da key pair, vamos copiar a chave criada e
restringir as permissões:
$ mv ~/Downloads/wbruno.pem ~/.ssh/
$ chmod 400 ~/.ssh/wbruno.pem
Agora, para iniciar uma nova instância EC2, vamos em Launch Instance e
escolheremos Amazon Linux 2 AMI, (Figura 8.6).
Figura 8.5 – Criando uma key pair.
Figura 8.6 – Amazon Linux 2 AMI.
Vamos usar uma t2.micro (Figura 8.7) por ser elegível ao free tier (ou seja, se
estivermos nos primeiros 12 meses de uso da AWS, não pagaremos).
Arquivo /etc/systemd/system/livro_nodejs.service
[Unit]
Description=livro nodejs
After=network.target
StartLimitIntervalSec=0
[Service]
Type=simple
Restart=áster
RestartSec=1
User=ec2-user
Environment="NODE_CONFIG_DIR=/var/www/livro_nodejs/config/"
ExecStart=/usr/bin/env /home/ec2-user/.nvm/versions/node/v15.5.1/bin/node
/var/www/livro_nodejs/server/bin/www.js
[Install]
WantedBy=multi-user.target
Crie o arquivo com sudo vim:
[ec2-user@... livro_nodejs]$ sudo vim /etc/systemd/system/livro_nodejs.service
E inicie o serviço:
[ec2-user@... livro_nodejs]$ sudo systemctl start livro_nodejs
[ec2-user@... livro_nodejs]$ sudo systemctl status livro_nodejs
ü livro_nodejs.service - livro nodejs
Loaded: loaded (/etc/systemd/system/livro_nodejs.service; disabled; vendor preset:
disabled)
Active: active (running) since Thu 2021-01-07 15:48:53 UTC; 10min ago
Main PID: 6797 (node)
Cgroup: /system.slice/livro_nodejs.service
├─6797 /home/ec2-user/.nvm/versions/node/v15.5.1/bin/node
/var/www/livro_nodejs/server/bin/www.js
└─6808 /home/ec2-user/.nvm/versions/node/v15.5.1/bin/node
/var/www/livro_nodejs/server/bin/www.js
Arquivo /etc/nginx/conf.d/livro_nodejs.conf
upstream nodejs {
server 127.0.0.1:3000;
}
server_tokens off;
charset utf-8;
gzip on;
gzip_disable "msie6";
gzip_min_length 1;
gzip_types *;
gzip_http_version 1.1;
gzip_vary on;
gzip_comp_level 6;
gzip_proxied any;
gzip_buffers 16 8k;
server {
listen 80;
server_name _;
access_log /var/log/livro_nodejs/access.log;
error_log /var/log/livro_nodejs/error.log;
# server_name site.com.br www.site.com.br;
# if ($http_host != "site.com.br") {
# rewrite ^ http://site.com.br$request_uri ásteree;
#}
root /var/www/livro_nodejs/public/;
error_page 404 500 502 503 504 /50x.html;
location /50x.html {
internal;
}
location / {
add_header ástere-security-policy "default-src 'self';base-uri 'self';block-all-mixed-
content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src
'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-
insecure-requests";
add_header x-content-security-policy "default-src 'self';base-uri 'self';block-all-mixed-
content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src
'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-
insecure-requests";
add_header x-webkit-csp "default-src 'self';base-uri 'self';block-all-mixed-content;font-
src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src
'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests";
add_header x-dns-prefetch-control off;
add_header expect-ct "max-age=0";
add_header x-frame-options SAMEORIGIN;
add_header strict-transport-security "max-age=15552000; includeSubdomains";
add_header x-download-options noopen;
add_header x-content-type-options nosniff;
add_header x-permitted-cross-domain-policies none;
add_header referrer-policy no-referrer;
add_header x-xss-protection "1; mode=block";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://nodejs;
}
location ~* \.(?:ico|css|html|json |js|map|gif|jpe?g|png|ttf|woff|woff2|svg|eot|txt|csv)$ {
access_log off;
expires 30d;
add_header pragma public;
add_header Cache-Control "public, mustrevalidate, proxy-revalidate";
}
}
Agora, inicie o serviço:
[ec2-user@... livro_nodejs]$ sudo systemctl start nginx
[ec2-user@... livro_nodejs]$ sudo systemctl status nginx
ü nginx.service - The nginx HTTP and reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset:
disabled)
Active: active (running) since Thu 2021-01-07 16:42:58 UTC; 4min 2s ago
Process: 7518 ExecStart=/usr/sbin/nginx (code=exited, status=0/SUCCESS)
Process: 7514 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=0/SUCCESS)
Process: 7513 ExecStartPre=/usr/bin/rm -f /run/nginx.pid (code=exited,
status=0/SUCCESS)
Main PID: 7520 (nginx)
Cgroup: /system.slice/nginx.service
├─7520 nginx: áster process /usr/sbin/nginx
└─7521 nginx: worker process
Feito isso, o Nginx está fazendo proxy reverso e servindo na porta 80; logo,
podemos fazer o request sem informar a porta:
$ curl http://18.231.94.3/troopers
[{"_id":"5ff71762860c8d05c4479f3c","name":"FN-2187","nickname":"Finn"}]
Agora, é uma boa prática voltar ao security group e remover a liberação da
porta 3000.
8.8.3 aws-cli
Caso queira instalar o aws cli no OS X, execute os comandos a seguir:
$ curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
$ sudo installer -pkg AWSCLIV2.pkg -target /
$ aws configure
E, com o comando EC2 describe-instances, podemos ver quais máquinas
lançamos:
$ aws ec2 describe-instances --query "Reservations[].Instances[].
{InstanceId:InstanceId,KeyName:KeyName,StateName:State.Name,PlacementAvailabilityZone:Placem
[
{
"InstanceId": "i-05aefb02265846db0",
"KeyName": "wbruno",
"StateName": "stopped",
"PlacementAvailabilityZone": "as-east-1c"
}
]
Fiz um filtro com --query para trazer as informações mais relevantes. Lembre-
se: desligue colocando em stop ou terminate a EC2 para evitar cobranças!
8.9 Heroku
Se utilizarmos um servidor no modelo PaaS (Plataform as a Service), não
vamos precisar instalar o NodeJS nem configurar o Nginx e o Unix Service,
como faríamos em um IaaS (Infraestructure as a Service). O host nos
fornecerá tudo isso de uma forma transparente.
O Heroku (https://heroku.com/) oferece um ótimo PaaS para diversas
linguagens de programação, inclusive NodeJS
(https://devcenter.heroku.com/articles/getting-started-with-nodejs), e
podemos utilizá-lo de graça para subir nossas aplicações de teste. Basta criar
uma conta no Heroku e escolher qual tipo de integração fará.
No canto superior direito, vá em New, Create new app (Figura 8.11).
8.10 Travis CI
O Travis CI (https://www.travis-ci.com) é um ótimo serviço de integração
contínua bem simples de configurar. Para projetos públicos no GitHub, ele é
gratuito. Vá até https://www.travis-ci.com e crie uma conta conectada ao seu
profile do GitHub (https://github.com).
Depois disso, crie o arquivo .travis.yml na raiz do projeto com o seguinte
conteúdo:
Arquivo .travis.yml
language: node_js
node_js:
- 15
env:
- NODE_ENV=test
Por se tratar de um projeto NodeJS, o Travis CI sabe que deve invocar o
comando npm test para executar os testes da aplicação. Simples assim. Você
pode configurar o Travis CI para rodar os seus testes unitários a cada push no
repositório (Figura 8.13).