Construindo Aplicações Com NodeJS - 3 Edição - William Bruno Moraes - 2021

Fazer download em pdf ou txt
Fazer download em pdf ou txt
Você está na página 1de 311

3ª Edição

William Bruno Moraes

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 2 Ferramentas de linha de comando


2.1 Seu primeiro programa
2.2 Debug
2.3 Brincando com TCP
2.4 Criando um servidor HTTP
2.4.1 Outros endpoints
2.5 Nodemon
2.6 Express Generator
2.7 Método process.nextTick
2.8 Star Wars API

capítulo 3 REST (Representational State Transfer)


3.1 Exemplos de APIs
3.2 Estrutura da requisição
3.3 Estrutura da resposta
3.4 Restrições do REST
3.5 Testando a requisição com curl
3.6 Testando a requisição com o Postman ou Insomnia

capítulo 4 Bancos de dados


4.1 Postgres
4.1.1 Modelagem
4.1.2 node-postgres
4.2 MongoDB
4.2.1 Modelagem
4.2.2 mongoist
4.3 Redis
4.3.1 Modelagem
4.3.2 node-redis

capítulo 5 Construindo uma API RESTful com


ExpressJS
5.1 ExpressJS
5.2 Middlewares
5.2.1 Entendendo a utilidade
5.2.2 Tipos de resposta
5.3 Controllers
5.4 Melhorando o listener do servidor
5.4.1 Cluster
5.4.2 dnscache
5.4.3 HTTP keepalive
5.5 API Stormtroopers
5.5.1 mongoist
5.5.2 Mongoose
5.5.3 pg
5.6 Autenticação
5.6.1 PassportJS
5.6.2 JSON Web Token
5.7 Fastify
5.7.1 Schema
5.8 Serverless
5.9 JSON Schema

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 7 Testes automatizados


7.1 Criando testes de código
7.2 Jest
7.2.1 beforeAll,afterAll,beforeEach,afterEach
7.2.2 ESlint
7.3 Testes unitários
7.4 Testes funcionais
7.5 Testes de carga

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

Gostaria de agradecer ao editor Rubens Prates, pela oportunidade de escrever


este livro; a Priscila Yoshimatsu, pelas dicas e correções durante o processo
de criação; ao meu amigo Márcio Hanashiro, pela foto da capa; a todos os
leitores das edições anteriores que com o seu feedback me fizeram ter
vontade de melhorar o livro para a terceira edição; aos meus alunos, a quem
tive o prazer de ensinar; e a todos os profissionais com quem trabalhei lado a
lado nesses anos.
Sobre o autor

William Bruno Moraes é desenvolvedor web apaixonado por boas práticas e


arquitetura de software. Iniciou em programação web em 2008 com HTML,
CSS, JavaScript e PHP. Participante ativo do Fórum iMasters
(https://forum.imasters.com.br/profile/69222-william-bruno/), escreve artigos
em seu blog pessoal (http://wbruno.com.br) e alguns outros canais.
Atualmente, trabalha na plataforma de Ecommerce do Grupo Boticário, com
microsserviços.
Suas primeiras linguagens de programação foram C e Assembly, que viu no
curso técnico em Eletrônica. Anos mais tarde, após um semestre de
Introdução a Orientação a Objetos na faculdade e um curso de PHP e MySQL
no Senac, estudou tableless com o primeiro livro do Maujor e iniciou sua
carreira como desenvolvedor web.
Quando começou, ainda não existia distinção entre programadores backend e
frontend, então se acostumou a programar sites e sistemas completos, desde o
recorte do psd até a modelagem do banco de dados, a programação server-
side da aplicação e deploy em produção.
Sempre gostou de ensinar, por isso já respondeu a milhares de tópicos no
Fórum iMasters. Teve a oportunidade de ministrar aulas de frontend, NodeJS
e MongoDB para turmas de cursos livres, além de ter dado algumas palestras
em eventos presenciais e online.
Com o convite para escrever este livro, viu a oportunidade perfeita para
divulgar a experiência que teve ao trabalhar com diversas linguagens de
programação e o porquê de gostar tanto da linguagem JavaScript e NodeJS.
Alguns links:
• Podcast DevNaEstrada: https://devnaestrada.com.br/2016/11/25/william-
bruno.html
• Como é trabalhar como Full-Stack, por William Bruno:
https://medium.com/trainingcenter/como-é-trabalhar-como-full-stack-
por-william-bruno-cc270386d3d2
• Minha história no desenvolvimento web:
http://wbruno.com.br/opiniao/minha-historia-desenvolvimento-web/
Prefácio

Muito tempo atrás, numa galáxia muito distante, tive a oportunidade de


ministrar um curso de 16 horas sobre NodeJS para uma turma no Centro de
Treinamento da Novatec. Escrevi uma apostila enquanto preparava o sistema
que desenvolveríamos durante o curso para ser o guia de conteúdo e
explicações. Então o Rubens Prates ofereceu-me a chance de transformar essa
apostila em livro. E aqui estamos na terceira edição, revisada e ampliada.
Quero, com este livro, repassar o conhecimento que adquiri ao trabalhar com
a plataforma NodeJS e outras linguagens (PHP, Java, Ruby e Python), das
quais incorporo conceitos e boas práticas – a experiência que adquiri ao
desenvolver diferentes projetos, os erros e os acertos –, apresentando a você
uma forma de desenvolvimento baseada nessa jornada, com projetos reais
que foram para produção e atenderam milhares de clientes.
Meu intuito não é escrever mais um tutorial de “Hello, World”, mas
solidificar a base de conhecimento sobre JavaScript, NodeJS, banco de dados
e HTTP para depois desenvolver uma API RESTful funcional com testes
unitários, que estará pronta para ser publicada em produção seguindo os
conceitos da plataforma NodeJS e otimizações. Utilizaremos uma estrutura
robusta, testável e expansível, que você poderá replicar como base para
implementar outras aplicações.

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.

Códigos dos exemplos


Os códigos utilizados nos exemplos e nas explicações deste livro estão
disponíveis no repositório GitHub: https://github.com/wbruno/livro-nodejs.
CAPÍTULO1
Introdução

Em 2009 Ryan Dahl apresentou o NodeJS na JSConf Europa. Essa


apresentação pode ser encontrada no YouTube. O NodeJS
(https://nodejs.org/) é uma poderosa plataforma, que possibilita construir
fácil e rapidamente aplicações de rede escaláveis. Utiliza a engine JavaScript
open source de alta performance do navegador Google Chrome, o motor V8
(https://developers.google.com/v8/), que é escrito em C++, e mais uma
porção de módulos, como a libuv.
Diferentemente da arquitetura do PHP (http://php.net), em que temos o PHP
como linguagem server-side e o Apache como servidor web, com NodeJS
nós escrevemos o nosso servidor web e também programamos a aplicação
server-side, tudo em JavaScript.
O NodeJS utiliza uma arquitetura orientada a eventos e um modelo de I/O
não bloqueante que faz com que ele seja leve e eficiente. Essas são
características perfeitas para solucionar os problemas de tráfego intenso de
rede e aplicações em tempo real, que são frequentemente os maiores desafios
das aplicações web hoje em dia.
Isso é possível devido à arquitetura de event loop (Figura 1.1), em que tudo é
processado em uma single thread, em vez de abrir uma nova thread para cada
requisição.
Figura 1.1 – Arquitetura event loop.
Por ter sido projetado para construção de aplicações de rede, é possível
desenvolver para qualquer protocolo: DNS, FTP, HTTP, HTTPS, SSH, TCP,
UDP ou WebSockets. Neste livro o foco é o protocolo HTTP.
Vemos o NodeJS ser frequentemente utilizado para construir aplicações web,
mas as suas aplicações vão além, como:
• escrever ferramentas de linha de comando;
• desenvolver aplicativos nativos para o sistema operacional (Windows,
Linux ou OS X) utilizando projetos como o NW.js
(https://github.com/nwjs/nw.js);
• escrever aplicativos mobile para iOS, Android ou Windows Phone,
usando o Ionic Framework (https://ionicframework.com) ou o React
Native (https://reactnative.dev).
O NodeJS é, portanto, um runtime de JavaScript, open source e
multiplataforma, que nos permite executar programas fora do browser, como
na nossa máquina local, num servidor, num Raspberry Pi etc.

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.

Figura 1.2 – Always bet on JS.

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.

Entendendo .bind(), .call() e .apply()


A função bind() retorna uma função alterando o escopo da função-alvo para
aquele que você passar como argumento. Digamos que eu tenha uma função
assim:
> function sith() { console.log(this); }
Ao invocar:
> sith()
<ref *1> Object [global] {
global: [Circular *1],
...
sith: [Function: sith]
}
O this aponta para o objeto root do NodeJS, que é o global. No browser
acontece o mesmo, mas o objeto root é window.
Com o .bind(), podemos alterar o escopo dessa função:
> var lordSith = sith.bind({ name: 'Darth Bane' });
undefined
> lordSith()
{ name: 'Darth Bane' }
undefined
O this agora foi o objeto que passei como argumento da função .bind().
Poderíamos enviar qualquer coisa como argumento (String, Number, Object
etc.):
> var lordSith = sith.bind('Darth Bane'); //String
undefined
> lordSith()
[String: 'Darth Bane']
undefined
> var lords = sith.bind(19); //Number
undefined
> lords()
[Number: 19]
undefined
Fica claro que o .bind() retorna uma nova função com um novo escopo.
A função .call() não retorna. Ela executa a função no momento em que for
chamada:
> function sith() { console.log(this); }
undefined
> sith.call({ name: "Darth Maul" });
{ name: 'Darth Maul' }
undefined
Por isso, conforme o contexto e o momento em que queremos que algo seja
executado, utilizamos o .bind() no lugar do .call(). A função .apply() tem o
mesmo comportamento do .call():
> sith.apply({ name: "Darth Vader" });
{ name: 'Darth Vader' }
undefined
A verdadeira diferença entre .call() e .apply() está no segundo argumento.
Enquanto a .call() recebe uma lista de argumentos que será repassada como
argumentos da função em que foi chamada:
> function sith(arg1, arg2, arg3, arg4) { console.log(this); console.log(''+ arg1 + arg2 +
arg3 + arg4); }
undefined
> sith.call({ name: "Darth Sidious" }, 1,1,3,8);
{ name: 'Darth Sidious' }
1138
undefined
a função .apply() recebe um array:
> function sith(arg1, arg2, arg3, arg4) { console.log(this); console.log(''+ arg1 + arg2 +
arg3 + arg4); }
undefined
> sith.apply({ name: "Darth Vectivus" }, [1,1,3,8]);
{ name: 'Darth Vectivus' }
1138
undefined
Agora, ao utilizar com arrow functions, note que o .bind não tem o mesmo
efeito:
> const jedi = () => { console.log(this) }
Undefined
> jedi()
<ref *1> Object [global] {

>
> jedi.bind({ name: 'Obi-Wan Kenobi'})()
<ref *1> Object [global] {

}
Tanto executando diretamente a função jedi que foi resultado da atribuição da
arrow function, quanto executando após um .bind de outro objeto, o this
continua sendo uma referência ao objeto do nível superior.
Por isso foi criada a propriedade global globalThis
(https://developer.mozilla.org/pt-
BR/docs/Web/JavaScript/Reference/Global_Objects/globalThis). Esta sempre
retorna o objeto global de mais alto nível. Em um browser, isso é verdade:
> globalThis === window
< true
Enquanto no NodeJS, isso é verdade:
> globalThis === global
true
Com o comando a seguir, a lista de flags e o estado de cada funcionalidade
serão listados:
$ node --v8-options|grep "harmony"

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

1.1.5 Controles de fluxo


Conhecemos de outras linguagens if, else, switch/case. A sintaxe no JavaScript
é idêntica à da linguagem C.
let justDoIt = 'try';
if (justDoIt === 'try') {
console.log('do')
} else {
console.log('do not')
}
'do'
Um bloco switch/case basicamente serve como uma cadeia de if, elseif, else.
let justDoIt = 'try'
switch (justDoIt) {
case 'try':
console.log('do')
break;
default:
console.log('do not')
break;
}
'do'

1.1.6 Laços de repetição


Existem diversas opções – for, for in, for of, while, do while – e os métodos de
coleções forEach, map, filter etc. Apesar de ser praticamente possível escrever
qualquer tipo de loop com for ou while, com experiência você achará qual das
opções é a mais adequada a cada situação.
Loop for
> const arr = [1,2,3,5,7,11];
> for (let i = 0, max = arr.length; i < max; i++) {
console.log(arr[i]);
}

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.9 Destructuring assignment


Atribuição via desestruturação é uma nova sintaxe para extrair dados de
arrays ou objetos em novas variáveis, podendo até colocar valores padrão,
caso undefined (https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment).
> const [a, b] = [1,2]
undefined
>a
1
>b
2
> const { name } = { name: 'Fin' }
undefined
> name
'Fin'
Podemos usar destructuring em argumentos de funções e para extrair em
variáveis apenas uma parte específica do objeto informado:
> function onlyNameAge({ name, age }) { console.log(name, age) }
undefined
> onlyNameAge({ name: 'Pedro Pascal', age: 45, country: 'Chile' })
Pedro Pascal 45
Note que a chave country foi completamente ignorada.

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.11 Rest parameters


A sintaxe rest parameters permite abandonar o uso da palavra reservada
arguments, com a vantagem de um rest parameter ser um array e ter todos os
métodos disponíveis para uso: .map, .forEach etc. A forma correta de uso é
somente como último argumento.
> function print(normalParam, ...restParam) { return normalParam + restParam.join(' ') }
undefined
> sum('impares: ', 1, 3, 5)
'impares: 1 3 5'
> sum('pares: ', 2, 4, 6)
'pares: 2 4 6'
Assim, podemos enviar um número arbitrário de argumentos, e todos eles
estarão dentro do array restParam.
1.1.12 Optional chaining
O operador ?. permite a leitura do valor de propriedades de objetos, sem
causar erro, se a referência for null ou undefined, enquanto usando o operador .
(ponto) causaria. É a forma de evitar verificação de nulo provendo navegação
segura.
Dado o seguinte 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' }
]
}
Podemos navegar entre as propriedades:
> televisionSerie.createdBy.name
'Jon Favreau'
Porém, ao tentar acessar o valor de uma propriedade que não existe:
> televisionSerie.composer.name
Uncaught TypeError: Cannot read property 'name' of undefined
estoura um TypeError, pois não existe a propriedade composer; portanto, não
há como acessar o valor de .name.
Então, para lidar com essa situação em que precisamos ler uma propriedade,
mas não temos garantia se a navegação entre propriedades é segura,
empregávamos técnicas como:
> televisionSerie.composer && televisionSerie.composer.name
undefined
> (televisionSerie.composer || {}).name
undefined
que, ao longo do tempo, iam deixando o código verboso e feio. Felizmente
agora, com ES6, podemos usar ?. (interrogação ponto):
> televisionSerie.composer?.name
undefined

1.1.13 Default argument


Argumentos padrões possibilitam que uma função seja invocada sem valores:
> function defaultArg(answer = 42) { return answer }
undefined
> defaultArg()
42
> defaultArg(10)
10
isso explicita a diferença entre undefined e null:
> defaultArg(undefined)
42
> defaultArg(null)
null
null é um valor, enquanto undefined não é, podendo ser pensado como a falta
de valor.

1.1.14 use strict


A flag use strict é uma declaração que foi criada para informar ao interpretador
que o nosso código JavaScript seguirá boas práticas. Assim, o interpretador
nos avisará de alguns erros comuns, como: não declarar variáveis, desabilitar
o uso de palavras reservadas em variáveis ou funções (arguments, eval etc.).
Deve ser sempre a primeira linha de cada arquivo com extensão .js.
Arquivo use-strict.js
'use strict'
sum='1+1'
console.log(eval(sum))
Executando no terminal:
$ node use-strict.js
/…/capitulo_1/1.1.14/use-strict.js:2
sum='1+1'
^
ReferenceError: sum is not defined
Atribuímos um valor a sum, sem declarar, sem dizer se é var, let ou const, mas,
se não tivéssemos declarado o modo estrito, executaria sem nenhum erro.
Arquivo non-use-strict.js
sum='1+1'
console.log(eval(sum))
Executando:
$ node non-use-strict.js
2
No Mozilla Developer Network, é possível ler mais sobre cada aspecto da
linguagem e o strict mode (https://developer.mozilla.org/pt-
BR/docs/Web/JavaScript/Reference/Strict_mode).

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

1.2 Instalação do NodeJS


Siga as instruções referentes ao seu sistema operacional:
https://nodejs.org/en/download/package-manager/.
Se você estiver utilizando Windows, lembre-se de reiniciar a máquina após a
instalação (ou fazer logoff e logon). Outra dica legal é instalar o Git Bash
(https://git-scm.com/downloads), para ter um terminal Unix-like.
Verifique a versão do NodeJS instalada com o comando a seguir no seu
terminal:
$ node --version && npm --version
v15.5.0
7.3.0
Em 2015, enquanto eu escrevia a primeira versão deste livro, existia um
projeto chamado io.js que posteriormente foi incorporado dentro do
repositório oficial do NodeJS, sob governança do Node.js Foundation. O
io.js, que nasceu em janeiro de 2015, baseado no NodeJS, foi criado com a
intenção de lançar novas versões com maior frequência, assim como se
manter atualizado com as versões do motor v8 e, consequentemente, com as
especificações da ECMAScript.
O movimento para a unificação dos dois projetos (o NodeJS que estava sob
governança da Joyent, e o io.js que estava nas mãos da comunidade Open
Source) teve início em 15 de março de 2015 e foi feito em um novo
repositório (https://github.com/nodejs/node). Por um tempo, o NodeJS e o
io.js continuaram a coexistir e a lançar novas versões até que a Node
Foundation tivesse terminado de realizar a unificação dos dois projetos.
A unificação fez o NodeJS pular da versão 0.12.7 para a versão 4.0 e assim
não conflitar com as versões antigas do io.js.
Na Figura 1.3 vemos um diagrama do calendário de lançamento e suporte de
versões.
Figura 1.3 – Calendário LTS do NodeJS.
Desde que o merge com o io.js foi concluído, a Node Foundation começou
um programa de LTS (Long Term Support) que definiu que as versões pares
(v4, v6 etc.) seriam as LTS estáveis, e as versões ímpares (v5, v7 etc.) as de
desenvolvimento. Assim, as novas features chegariam mais rapidamente
porque seriam atualizadas constantemente, enquanto as versões pares
recebem patchs de segurança e possuem um ciclo menor de releases.
O NodeJS segue o versionamento semântico (http://semver.org/lang/pt-BR/)
que define uma série de regras e funciona da seguinte maneira:

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.

1.2.1 Módulos n e nvm


Após instalar o NodeJS pela primeira vez, você pode atualizar para novas
versões, reinstalar uma versão antiga ou alternar entre NodeJS e io.js com
ajuda de módulos disponíveis no npm. Existem dois módulos que fazem esse
trabalho, o n (https://www.npmjs.com/package/n), que não foi feito para
funcionar no Windows, e o nvm (https://www.npmjs.com/package/nvm), que
é multiplataforma.
Você instala o n via npm (Node Package Manager) e depois controla a versão
do NodeJS que quer utilizar.
$ npm install n –g
A flag -g indica que estamos instalando esse módulo globalmente. Com isso,
ele estará disponível para executar diretamente via linha de comando a partir
de qualquer diretório na máquina. Alguns comandos disponíveis para instalar
versões são:
$ n stable # instala a versão estável disponível
$ n latest # instala a última versão disponível (não estável)
$ n 15.5.0 # instala exatamente a versão especificada
E com o comando n conseguimos alternar entre as versões instaladas:
$n
node/8.0.0
node/15.5.0
O nvm você instala conforme o sistema operacional que for utilizar, existe o
nvm-windows (https://github.com/coreybutler/nvm-windows) para Windows,
e para Mac OS X ou Linux (https://github.com/nvm-sh/nvm), é possível
instalar via curl ou wget:
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash
$ wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash
A utilização do nvm é bem parecida com a do n:
$ nvm install --lts # instala a versão estável disponível
$ nvm install 15.5.0 # instala exatamente a versão especificada
Outra funcionalidade legal do nvm é que você pode ter um arquivo .nvmrc na
raiz do projeto, indicando qual versão do NodeJS é utilizada naquele projeto.
$ cat .nvmrc
14.15.3
Dessa forma, os demais programadores digitam nvm install ou nvm use nesse
projeto, e o nvm irá ler o arquivo .nvmrc para instalar e ativar exatamente a
versão do NodeJS indicada.

1.2.2 Arquivo package.json


Todo projeto NodeJS tem um arquivo package.json na raiz. Esse arquivo
contém informações sobre a aplicação, como nome, versão, descrição,
repositório Git e dependências.
Use o comando npm init --yes para criar a estrutura inicial do package.json.
$ npm init --yes
Um arquivo mais ou menos parecido com este será criado:
Arquivo package.json
{
"name": "1.2.2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "William Bruno <[email protected]> (http://wbruno.com.br)",
"license": "ISC"
}
O atributo name não deve conter caracteres especiais ou espaços. Use letras
minúsculas e separe as palavras com hifens ou underlines. Lembrando que
você sempre pode editar o arquivo package.json a qualquer momento.

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.

1.3 NPM (Node Package Manager)


O npmjs (https://www.npmjs.com) é um serviço (grátis para pacotes públicos)
que possibilita aos desenvolvedores criar e compartilhar módulos com outros
desenvolvedores, dessa forma facilitando a distribuição de códigos e
ferramentas escritas em JavaScript (https://docs.npmjs.com/getting-
started/what-is-npm).
E o npm é uma ferramenta de linha de comando, que gerencia pacotes do
registry npmjs.
Por exemplo, se você quiser utilizar o ExpressJS (http://expressjs.com), que é
um framework rápido e minimalista para construção de rotas em NodeJS, não
é necessário entrar no GitHub do projeto, fazer download do zip ou clonar o
repositório para depois instalar em um diretório da sua máquina. Basta digitar
no seu terminal:
$ npm install express
ou apenas:
$ npm i express
trocando o install pela letra i, como atalho para digitar menos.
Com esse comando, o módulo será baixado para uma pasta chamada
node_modules no diretório raiz do projeto, ou seja, ao lado do arquivo
package.json mais próximo.
Você pode criar uma conta no site www.npmjs.com, caso pretenda publicar
algum módulo no diretório.
Vale configurar na sua máquina local os seus dados no npm
(https://docs.npmjs.com/cli/config). Se você estiver em um Linux ou OS X,
crie ou edite um arquivo .npmrc na raiz do seu usuário:
$ cat ~/.npmrc
init.author.name=William Bruno
[email protected]
init.author.url=http://wbruno.com.br
No Windows, o arquivo npmrc estará localizado dentro da pasta npm da
instalação do NodeJS em: C:\Program
Files\nodejs\node_modules\npm\npmmrc ou em C:\Users\
<user>\AppData\Roaming\npm.

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.1 npm update


A todo momento, novas versões dos módulos que utilizamos estão sendo
desenvolvidas, com correções, melhorias de performance e novas
funcionalidades, por isso é importante manter atualizadas as dependências
dos nossos projetos, dentro do possível.
O comando npm update (https://docs.npmjs.com/cli/update) nos ajuda a
verificar quais módulos podem ser atualizados.
$ npm update --save
A flag --save vai se encarregar de escrever no package.json a nova versão dos
módulos que foram atualizados.
Existe uma diferença entre os símbolos ^ e ~ que podem vir na frente da
versão de cada módulo no package.json. Basicamente, o ~ é mais restritivo e só
permite atualizações patch (último dígito do número da versão), enquanto o ^
permite que atualizações minor (segundo dígito do número da versão) sejam
realizadas.
Isso existe porque uma atualização major (primeiro número do número da
versão) pode quebrar a compatibilidade, por isso esse tipo de atualização
deve ser feito manualmente, com bastante cuidado, após ter uma boa
cobertura de testes. E, não colocando nenhum desses símbolos, trava a
dependência na versão exata.
Trecho do arquivo package.json
"dependencies": {
"mongoist": "^2.5.0"
}
A dependência mongoist, por exemplo, está na versão 2.5.0, mas o último
release disponível no GitHub do desenvolvedor é a versão 2.5.3, o npm update
irá atualizar para 2.5.3, pois o ^ (acento circunflexo) permite.
É sempre bom dar uma conferida se algum pacote pode ser atualizado, lendo
o changelog do projeto, inclusive se a versão do NodeJS instalada no servidor
e na sua máquina local de desenvolvimento pode ser atualizada.

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.

Learn you Node


Existe um módulo npm chamado learnyounode
(https://github.com/workshopper/learnyounode) mantido pela NodeSchool
(http://nodeschool.io), que ensina conceitos básicos de NodeJS por meio de
uma interface interativa dentro do seu terminal. Instale globalmente o
módulo:
$ npm install -g learnyounode
E com o comando
$ learnyounode
execute o programa que irá abrir no terminal um menu com vários exercícios.
Cada exercício apresenta uma explicação para você desenvolver um código
que será testado para dizer se você resolveu ou não a questão. Ou apenas
execute, sem instalar globalmente com o npx:
$ npx learnyounode

1.3.3 yarn (Package Manager)


O yarn (https://yarnpkg.com) é uma alternativa ao cli npm. Deve ser instalado
como pacote global, depois explicitamente dito que queremos a versão 2.
$ npm i -g yarn
$ yarn set version 2
$ yarn -v
2.4.0
Podemos utilizar yarn install em vez de usar npm i para gerenciar as
dependências dos nossos projetos. Da mesma forma que o npm cria um
package-lock.json, o yarn cria um arquivo yarn.lock para garantir as versões
internas das dependências instaladas.
O comando yarn dlx é o equivalente ao npx:
$ yarn dlx cowsay '4 de maio'
E o desenho ASCII no terminal é o mesmo, conforme vemos na Figura 1.5.

Figura 1.5 – Resultado no terminal do comando yarn dlx cowsay.


Se irá usar npm ou yarn, depende de uma análise da equipe. Na Figura 1.6
vemos uma tabelinha comparativa dos comandos.

1.4 Console do NodeJS (REPL)


O NodeJS disponibiliza uma forma para acessar as propriedades e funções
utilizando o terminal do sistema operacional sem que seja necessário escrever
e salvar códigos em um arquivo. Isso é muito útil para testar pequenos
trechos de código e entender como as coisas funcionam. Este é o REPL
(https://nodejs.org/api/repl.html), também chamado de console.
Apesar de ser possível utilizar o próprio prompt cmd do Windows, existe
uma alternativa chamada Git Bash (https://git-scm.com/downloads), que lhe
dará uma experiência mais próxima do terminal de sistemas com base Unix.
Figura 1.6 – Tabela comparativa do npm vs yarn.
Caso você esteja em um OS X ou em alguma distribuição Linux, o terminal
padrão já oferece todas as ferramentas necessárias.
Para entrar no console do NodeJS, basta digitar node no terminal:
$ node
O console aceita qualquer expressão JavaScript válida, como:
> 4 + 2;
6
Na próxima linha, será mostrado o resultado ou o retorno da expressão que
escrevermos.
É possível ainda usar a flag -p (evaluate script and print result), para executar
código NodeJS fora do REPL, diretamente no terminal.
$ node -p "require('./package.json').name"
graphql

1.4.1 Variáveis de ambiente


Variáveis de ambiente são usadas para fornecer informações adicionais aos
nossos scripts, como o ambiente em que o sistema está rodando, algum dado
de configuração do banco de dados ou o caminho até o arquivo de log, por
exemplo. Pertencem ao sistema operacional que estamos utilizando e ficam
armazenadas em um espaço da memória RAM da máquina.
Se você estiver trabalhando em Unix (OS X ou Linux), use o seguinte
comando para criar ou alterar o valor de uma variável de ambiente, atente que
não existem espaços entre o nome da variável, o sinal de igual e o valor dela.
Deve ser exatamente assim a declaração de uma variável de ambiente:
$ export NODE_ENV=development
Porém, se você estiver trabalhando em uma máquina Windows, use set no
lugar de export:
$ set NODE_ENV=development
Para verificar o valor de uma variável de ambiente, podemos imprimi-la,
colocando o símbolo $ na frente do nome da variável:
$ echo $NODE_ENV
development
No Windows seria:
$ echo %NODE_ENV%
Development
Executando com NodeJS:
$ node -p "process.env.NODE_ENV"
development
Sempre que iniciar uma linha de exemplo com >, entenda que estamos dentro
do terminal do NodeJS:
$ node
> process.version
'v15.5.0'
> process.versions.v8
' 8.6.395.17-node.23'
> process.env.NODE_ENV
'development'
O objeto process é um objeto global do NodeJS que podemos utilizar,
inclusive, em nossos scripts. É útil acessar a propriedade .env (environment)
para identificar em qual ambiente nosso projeto está rodando, seja
desenvolvimento, homologação ou produção.
Imagine uma situação em que você precise testar uma expressão regular e não
queira criar um arquivo só para isso. Você pode utilizar o console do NodeJS
direto no terminal:
$ node
> /rato/g.test('O rato roeu a roupa')
true
Comecei simples, como qualquer boa regex. O padrão /rato/ casou com o
texto 'O rato roeu a roupa'. Se mudássemos de rato para gato, teríamos um false:
> /rato/g.test('O gato comeu a roupa')
false
Se quisermos que a regex case tanto com a frase 'O rato roeu a roupa' quanto
com a frase 'O gato comeu a roupa', basta alterar para:
> /(g|r)ato (ro|com)eu/g.test('O gato comeu a roupa')
true
> /(g|r)ato (ro|com)eu/g.test('O rato roeu a roupa')
true
Poderíamos testar alguns scripts para somar os números de um array:
> var arr = [1,2,3,4,5,6], sum = 0;
undefined
> for(var i=0, max=arr.length; i<max; i++) { sum += arr[i]; }
21
> sum
21
Não ficou legal. Que tal utilizar o .forEach()?
> var arr = [1,2,3,4,5,6], sum = 0
undefined
> arr.forEach(function(n){ sum += n })
undefined
> sum
21
Utilizando arrow functions seria:
> var arr = [1,2,3,4,5,6], sum = 0
undefined
> arr.forEach((n) => { sum += n })
undefined
> sum
21
>
Ou a função reduce()?
> var arr = [1,2,3,4,5,6], sum = 0;
undefined
> sum = arr.reduce((prev, curr) => prev + curr)
21
Após o var arr =… aparece um undefined porque a atribuição de variável não
tem nenhum retorno.
Para escrever na saída (stdout), utilizamos a função console.log():
> console.log('Darth Vader');
Darth Vader
undefined
O primeiro retorno após a chamada da função console.log(), com a mesma
string que passamos como argumento, é a saída, e a linha após esta, com o
undefined, é o retorno.
É bem parecido com o console dos navegadores – quem é frontend vai
entender isso –, onde também podemos fazer todas essas coisas.
Mas, diferentemente dos browsers, veja que estamos no NodeJS, então o
objeto window, global do navegador, não existe:
> window
Uncaught ReferenceError: window is not defined
Retornou um ReferenceError quando tentei acessar um objeto chamado window,
que é o root, por assim dizer, do JavaScript quando ele roda dentro de um
navegador. Qualquer variável, função ou objeto que criarmos no escopo
global será descendente de window.
Na Figura 1.7 vemos o console do navegador Safari.
Figura 1.7 – Console do browser com o objeto window.
No NodeJS, o objeto root se chama global:
> global.process.env.NODE_ENV
'development'
E, da mesma forma que podemos ocultar window, quando invocarmos algum
objeto do escopo global, também poderemos ocultar o global no NodeJS. Caso
você queira ver tudo o que tem no objeto global, basta digitar global no
console:
> global
<ref *1> Object [global] {
global: [Circular *1],
clearInterval: [Function: clearInterval],
clearTimeout: [Function: clearTimeout],
setInterval: [Function: setInterval],
setTimeout: [Function: setTimeout] {
[Symbol(nodejs.util.promisify.custom)]: [Getter]
},
queueMicrotask: [Function: queueMicrotask],
clearImmediate: [Function: clearImmediate],
setImmediate: [Function: setImmediate] {
[Symbol(nodejs.util.promisify.custom)]: [Getter]
}
}

1.5 Programação síncrona e assíncrona


Um procedimento síncrono é aquele em que uma operação ocorre após o
término de outra. Imagine um procedimento que demore dois segundos para
ser executado. A linha de código após esse procedimento não será executada
até que o procedimento da linha anterior tenha terminado de executar. Esse é
o comportamento previsível ao qual já estamos acostumados quando
trabalhamos com outras linguagens de programação.
Essa característica bloqueante garante que o fluxo de execução seja
exatamente na ordem que lemos as linhas do código.
Arquivo sincrono.js
console.log('1');
t();
console.log('3');
function t() {
console.log('2');
}
A saída no terminal será a ordem natural:
$ node sincrono.js
1
2
3
Um procedimento assíncrono não bloqueia a execução do código. Se um
procedimento levar certo tempo para ser encerrado, a linha após esse
procedimento será executada antes de o procedimento assíncrono terminar, e
isso geralmente é uma das maiores dificuldades na hora de programadores de
outras linguagens tentarem escrever JavaScript.
Arquivo assincrono.js
console.log('1');
t();
console.log('3');
function t() {
setTimeout(function() {
console.log('2');
}, 10);
}
A saída será 1, 3 e 2, pois a linha abaixo da invocação do console.log('3') não
irá aguardar a execução da função t(), ou seja, o fluxo não foi bloqueado:
$ node assincrono.js
1
3
2
Estamos apenas simulando um comportamento assíncrono, com o setTimeout.
No NodeJS, é preferível programar nossas rotinas de forma assíncrona para
não bloquear a main thread. É por isso que todas as funções e os métodos,
que fazem algum tipo de acesso a disco ou rede (input ou output), têm como
parâmetro uma função de callback.
Um callback é um aviso de que uma operação assíncrona terminou. É através
dele que controlamos o fluxo da nossa aplicação.
Arquivo writeFileSync.js
const fs = require('fs')
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.writeFileSync('sync.txt', text)
const data = fs.readFileSync('sync.txt')
console.log(data.toString())
Ao executar, no terminal com o comando node writeFile.js, teremos criado um
arquivo sync.txt, lido o conteúdo e escrito na saída com o console.log.
$ node writeFile.js
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.
Utilizando a versão assíncrona desses métodos (escrever e ler arquivos do
sistema de arquivos), o código ficaria assim:
Arquivo writeFile.js
const fs = require('fs')
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('async.txt', text, (err, result) => {
fs.readFile('async.txt', (err, data) => {
console.log(data.toString())
})
})
Note que tive que encaixar callbacks para que a leitura do arquivo só
acontecesse quando a escrita estivesse finalizada, e aí sim, quando terminar
de ler, eu posso usar o console.log.

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.

1.6 Orientação a eventos


Basicamente, eventos são avisos de que algo aconteceu. Dentro do browser,
existem vários eventos, como o clique em um elemento, o envio de um
formulário, pressionar uma tecla, carregar um documento etc. No NodeJS, os
eventos podem ser: um erro ao conectar em um banco de dados, o
recebimento parcial de um conteúdo de um stream, um aviso de que tudo já
foi recebido etc.
Extrapolando em abstração, quero que você pense em eventos como uma
forma de o JavaScript implementar nativamente o design pattern Observer,
que diz que um objeto (Subject) contém uma lista de dependências
(Observers) e as notifica automaticamente quando alguma mudança de estado
ocorre. Quando um usuário clica em um link dentro de uma página web em
um browser, o link é o subject, e o nosso código que estava escutando esse
evento é o observer.
Além de utilizar os eventos do ambiente e dos módulos que importarmos,
podemos registrar nossos próprios eventos, desacoplando assim o nosso
código.
No caso do NodeJS, utilizamos o módulo EventEmitter
(https://nodejs.org/api/events.html):
const EventEmitter = require('events');
const subject = new EventEmitter();
subject.on('event', function(a, b) {
console.log(a, b, this);
});
subject.emit('event', 'Kylo', 'Ren');
E a saída será:
Kylo Ren EventEmitter {
_events: [Object: null prototype] { event: [Function (anonymous)] },
_eventsCount: 1,
_maxListeners: undefined,
[Symbol(kCapture)]: false
}
true
Não se preocupe com os detalhes do código. Outros módulos possuem
eventos como on('ready'), on('open'), on('close'), on('error') etc.

1.7 Orientação a objetos


A linguagem JavaScript possui suporte à orientação a objetos. Entenda que
orientação a objetos é muito diferente de orientação a classes, que é o que a
maioria dos programadores faz por aí. O fato de a linguagem não conter
classes (até a ECMA5, pois na ECMA6 foi introduzido o operador class, que
funcionará apenas como um syntatic sugar5) não significa que não
implemente ou não seja possível aplicar os conceitos e as boas práticas de
OO que já conhecemos; muito pelo contrário, já que alguns patterns, por
exemplo, são até mais facilmente implementados em JavaScript que em
outras linguagens que contêm a famigerada herança clássica.
A herança no JavaScript é baseada em prototype, e os objetos podem tanto
ser feitos a partir de uma função construtora quanto a partir de um JSON. É
importante saber trabalhar corretamente com os objetos do JavaScript, vale a
pena estudar as diferenças com leituras complementares, como o livro
Princípios de orientação a objetos em JavaScript
(http://www.novatec.com.br/livros/orientacaoobjetosjavascript/).
Existem apenas sete tipos primitivos no JavaScript: String, Number, BigInt,
Boolean, null, undefinede Symbol (https://developer.mozilla.org/pt-
BR/docs/Glossario/Primitivo). Porém, exceto por null e undefined, os outros
tipos primitivos podem ser transformados em objetos implícita ou
explicitamente, conforme o contexto e a forma de uso.
Tire um tempinho e leia esse texto de Douglas Crockford: JavaScript: a
menos entendida linguagem de programação do globo!
(http://javascript.crockford.com/pt/javascript.html).
Um objeto é algo que possui propriedades e faz coisas. O princípio de
orientação a objetos se destina a trazer o mundo real para a programação de
software, em que pensamos em objetos, o que fazem e como eles se
relacionam uns com os outros.
Podemos declarar novos objetos criados a partir de uma função construtora.
> function Droid(){}
> const r2d2 = new Droid();
> r2d2
Droid {}
Ou então em formato de JSON (objetos literais), em que teremos sempre uma
mesma instância, ou seja, um Singleton.
> const BattleDroid = {};
> BattleDroid
{}

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.

Entendendo o objeto literal


Cada vez que invocamos o operador new para uma função construtora,
obtemos uma nova instância de um objeto, ou seja, uma nova referência de
memória.
No entanto, para objetos criados com a notação literal, teremos sempre a
mesma instância, a mesma referência de memória.
> const BattleDroid = {}
> BattleDroid.attack = function() {};
[Function]
> BattleDroid
{ attack: [Function] }
Para atribuir novos métodos, basta colocar o nome do objeto seguido de um
ponto e ao nome do método atribuir uma função anônima. A forma de uso é
estática:
> BattleDroid.attack();

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.

1.8 Programação funcional


JavaScript, além de ser uma linguagem de processamento assíncrono,
orientada a eventos, possui suporte à orientação a objetos, a
metaprogramação e também é uma linguagem que implementa programação
funcional. Está bom ou quer mais?
Ao trabalhar com uma linguagem funcional, devemos pensar na computação
como uma série de etapas, fazendo composição de funções, tendo assim um
código muito mais declarativo do que imperativo.
O paradigma de programação funcional trata a computação como uma
avaliação de funções matemáticas, permitindo um código mais legível,
conciso e evitando efeitos colaterais. Uma linguagem funcional segue alguns
preceitos:
• estruturas de controle e operações abstraídas como funções;
• funções podem abstrair outras funções;
• variáveis e estados imutáveis, para evitar efeitos colaterais (side effects);
• funções retornam valores, e funções puras não causam efeitos colaterais;
• dada uma mesma entrada, sempre retorna a mesma saída;
• funções são objetos de primeira classe (HOF – High Order Function), ou
seja, podem ser parâmetros, valores ou retornos de outras funções.
Para entender um pouco o paradigma funcional, veja alguns conceitos
importantes:
Função pura
Uma função é pura quando, dada uma entrada, sempre retorna a mesma
saída, sem efeitos colaterais, tudo de que ela precisa faz parte do seu
próprio escopo.
Função de alta ordem
Uma coisa muito importante para que uma linguagem seja funcional é que
ela tenha suporte a funções de alta ordem, ou seja, que possamos atribuir
funções a variáveis, receber como argumento ou retornar como resultado de
outra função.
Closures
O escopo do JavaScript é baseado em funções ou blocos. O fato de uma
função interna acessar variáveis e parâmetros de um escopo acima do seu
próprio é chamado de closure. Utilizamos closures em JavaScript para
proteger o escopo simulando o que é conhecido como membro privado. No
caso do NodeJS, temos escopos por módulos (cada arquivo é isolado em si).

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.

Métodos forEach, map, reduce, filter, every, some, sort


São métodos do objeto Array do JavaScript muito úteis que lhe permitem
escrever loops de maneira mais explícita, concisa e objetiva.

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 Tenha um bom editor de código


Para trabalhar com NodeJS, você pode utilizar qualquer editor de que goste e
com o qual já esteja acostumado, seja o VIM (VI Improved), Sublime Text,
Notepad++, IntellijIDEA, Visual Studio Code etc.
Vou citar algumas dicas bem interessantes aqui.

1.9.1 Arquivo de preferências do Sublime Text 3


O Sublime Text (https://www.sublimetext.com) é um ótimo editor de códigos
que tem sido muito utilizado nos últimos anos por desenvolvedores web, e é
uma ótima opção para você programar NodeJS. Será utilizado para escrever
os códigos de exemplo deste livro.
Por meio do menu Sublime Text > Preferences > Settings, você personaliza o
Sublime Text (Figura 1.8).
Figura 1.8 – Menu Sublime Text > Preferences > Settings. Tela do Sublime
Text 3.
Veja a seguir o meu arquivo pessoal.
Arquivo Preferences.sublime-settings
{
"color_scheme": "Packages/Color Scheme - Default/Monokai.tmTheme",
"ensure_newline_at_eof_on_save": true,
"file_exclude_patterns":
[
".DS_Store",
"*.min.*"
],
"folder_exclude_patterns":
[
".git",
".vscode",
"node_modules"
],
"font_size": 13,
"highlight_modified_tabs": true,
"ignored_packages":
[
"Vintage"
],
"open_files_in_new_window": false,
"save_on_focus_lost": true,
"scroll_speed": 0,
"side_bar_width": 210,
"smart_indent": true,
"tab_size": 2,
"translate_tabs_to_spaces": true,
"trim_trailing_white_space_on_save": true,
"word_wrap": true
}
Ele contém algumas configurações bacanas, como:
• inserir uma nova linha no fim do arquivo;
• ignorar arquivos temporários do sistema operacional e *.min.* na listagem
e busca;
• ignorar diretórios que não precisamos ver enquanto estamos programando;
• desabilitar o Modo VIM, que permite utilizar o Sublime Text como se
fosse o VIM;
• salvar arquivos ao trocar o foco;
• indentar com dois espaços;
• remover espaços desnecessários ao salvar;
• quebrar linhas para se ajustarem à área visível do editor.

1.9.2 Arquivo de preferências do Visual Studio


Code
IDEs são poderosos editores de código com diversas funcionalidades, como
autocomplete, atalhos para objetos, funções ou arquivos, debug integrado etc.
O Visual Studio Code (https://code.visualstudio.com) também possui um
arquivo de configuração; por meio do menu Code > Preferences > Settings, você
o personaliza, como vemos na Figura 1.9.
Figura 1.9 – Menu Code > Preferences > Settings. Tela do VS Code.
Arquivo settings.json
{
"window.zoomLevel": 1,
"files.autoSave": "onFocusChange"
}
Lendo a documentação https://code.visualstudio.com/docs/editor/settings-
sync, você vê como customizar outros comportamentos do editor.
Outra forma é ter em cada projeto o arquivo .vscode/settings.json com as
definições necessárias, assim irá sobrescrever as configurações do editor para
esta configuração mais específica.
Arquivo .vscode/settings.json
{
"files.exclude": {
"node_modules": true,
"package-lock.json": true,
"yarn.lock": true,
}
}
O VS Code possui um suporte a debug
(https://code.visualstudio.com/docs/nodejs/nodejs-debugging) que permite
inspecionar nossos programas NodeJS, TypeScript e Javascript de forma bem
similar ao que fazemos com outras linguagens, colocando breakpoints,
verificando valores de variáveis e objetos em tempo de execução.
Não que seja totalmente necessário, pois um projeto com uma boa cobertura
de testes deve eliminar a necessidade de um debug nesse nível, mas vale a
pena mencionar, então, está aí. Não use como muleta.

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.

1.10 Plugin para visualização de JSON


Os navegadores atuais ainda não sabem como renderizar o conteúdo de um
JSON, então, para facilitar a visualização, seja no Chrome, Safari ou Firefox,
podemos instalar plugins que formatem o JSON de forma indentada e
colorida.
Basta pesquisar por JSON View na loja de aplicativos do Chrome (Plugins),
do Safari (Extensions) ou do Firefox (Complementos), como mostrado na
Figura 1.10.

Figura 1.10 – JSON View na Chrome Web Store.

1 Escopo de bloco: o contexto do que está entre chaves {}.


2 Hoisting: é o comportamento que o JavaScript tem de mover a declaração de variáveis
para o topo, permitindo usar variáveis antes de ser declarada.
3 Raiz do projeto: no mesmo nível do arquivo package.json.
4 Sobre npm vs npx em https://blog.rocketseat.com.br/conhecendo-o-npx-executor-de-
pacote-do-npm/
5 Syntactic sugar (açúcar sintático) é uma sintaxe alternativa da linguagem de programação
que torna mais concisa uma declaração.
2
CAPÍTULO
Ferramentas de linha de
comando

Ferramentas de linha de comando são programas geralmente escritos em shell


script (Unix), em C# (Windows), Python, Ruby ou NodeJS, para automatizar
ou facilitar tarefas executadas dentro do terminal.
Podemos escrever e executar as nossas ferramentas de linha de comando em
NodeJS desde que o tenhamos instalado em nossa máquina. Por estar
programando em JavaScript, o desenvolvimento e a sintaxe são simples e
divertidos.

2.1 Seu primeiro programa


No Capítulo 1, fizemos alguns testes diretamente no console do NodeJS, mas
de agora em diante nossos programas serão escritos em arquivos.
Crie um arquivo chamado hello.js com o seguinte conteúdo:
process.stdout.write('Han Solo\n');
Para executar, vá até seu terminal, navegue até o diretório em que você criou
o arquivo e digite:
$ node hello.js
Han Solo
Na linha seguinte aparece a string que passamos como argumento para a
função process.stdout.write(). Caso não quisesse entrar no diretório em que
salvei o arquivo, eu deveria informar o caminho completo até ele para o
NodeJS executar:
$ node /Users/wbruno/Sites/wbruno/livro-nodejs/capitulo_2/2.1/hello.js
Han Solo
Para facilitar, vamos preferir sempre utilizar o terminal no mesmo diretório
em que estamos trabalhando.
Geralmente, as ferramentas de linha de comando recebem opções após o
comando, algo como:
$ node hello2.js status port 42
Conseguimos ler esses argumentos por meio do array process.argv.
Crie o arquivo args.js com o seguinte conteúdo:
process.argv.forEach(arg => console.log(arg))
e execute no terminal:
$ node args.js status port 42
/Users/wbruno/.nvm/versions/node/v15.5.0/bin/node
/Users/wbruno/Sites/wbruno/livro-nodejs/capitulo_2/2.1/args.js
status
port
42
A primeira linha do retorno é o comando que utilizamos para executar o
programa; a próxima linha é o caminho completo até o nosso arquivo; e as
demais são os argumentos que passamos.
A função console.log() internamente faz uma chamada à função
process.stdout.write().

Consumindo a API loremipsum.net


A API http://loripsum.net retorna certa quantidade de texto lorem ipsum. Esse
texto é uma peça clássica em latim que a indústria gráfica, a web e a
editoração utilizam para preencher espaços antes de o conteúdo final ser
escrito para verificar a tipografia e a formatação.
O nosso programa fará uma requisição nessa API e criará um arquivo .html
com o conteúdo retornado. Para fazer a requisição, utilizaremos o módulo
http (https://nodejs.org/api/http.html) nativo do NodeJS.
A primeira tarefa que temos de realizar é informar que usaremos o módulo
http. Para isso, utilizaremos a função require(), atribuindo o módulo a uma
constante (variável que não pode ser reatribuída):
const http = require('http');
Daí em diante, podemos utilizar o módulo http dentro desse arquivo que
nomearemos como loremipsum.js.
Arquivo loremipsum.js
'use strict';
const http = require('http');
http.get('http://loripsum.net/api/1', res => {
let text = '';
res.on('data', chunk => {
text += chunk;
});
res.on('end', () => {
console.log(text);
});
})
.on('error', (e) => {
console.log('Got error: ' + e.message);
});
Ao executar no terminal:
$ node loremipsum.js
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sine ea igitur iucunde negat
posse se vivere? Quid turpius quam sapientis vitam ex insipientium sermone pendere?
Conferam avum tuum Drusum cum C. Poterat autem inpune; Duo Reges: constructio
interrete. Rationis enim perfectio est virtus; </p>
Após a chamada do programa, aparece o retorno da linha da chamada com o
valor do console.log() que colocamos no evento end da requisição, mostrando o
conteúdo retornado pela API, que é o texto em latim.
Caso tivéssemos informado uma URI inválida para o método .get():
http.get('http://invalido', res => {
O evento error seria chamado e o outro console.log(), da penúltima linha do
script, escreveria o erro no terminal.
$ node loremipsum.js
Got error: getaddrinfo ENOTFOUND invalido
Neste caso foi um ENOTFOUND (endereço não encontrado).
Ok, agora falta escrever em um arquivo o texto retornado pela API. Para isso,
precisaremos do módulo fs (https://nodejs.org/api/fs.html). Substituiremos o
console.log() pela função writeFile().

Arquivo modificado loremipsum.js


'use strict';
const http = require('http');
const fs = require('fs');
http.get('http://loripsum.net/api/1', (res) => {
var text = '';
res.on('data', (chunk) => {
text += chunk;
});
res.on('end', () =>{
fs.writeFile('lorem.html', text, () =>{
console.log('Arquivo pronto!');
});
});
})
.on('error', (e) => {
console.log('Got error: ' + e.message);
});
Assim que executarmos o nosso script:
$ node loremipsum.js
Arquivo pronto!
será criado um arquivo chamado lorem.html no mesmo diretório em que
estamos rodando o script. Bacana, não é?
Nosso programa escreve sempre apenas um parágrafo em um mesmo arquivo
predefinido dentro do código, o que não é muito flexível. Para deixar mais
dinâmico esse programa, que tal receber via argumentos da linha de comando
a quantidade de parágrafos e o nome do arquivo a ser criado?
Seguiremos algumas boas práticas na criação de programas bash:
• terá um comentário como cabeçalho que explicará o que é e como usar;
• caso seja chamado sem nenhum argumento, com um argumento inválido,
ou faltando argumentos obrigatórios, os argumentos disponíveis deverão
ser informados pelo programa.
Lembra que process.argv é um array com todos os argumentos? A posição 0 do
array é sempre node, a posição 1 é o caminho completo até o programa, e a
partir da posição 2 se iniciam os parâmetros que passamos no momento da
chamada. Verificaremos se os parâmetros foram passados antes de fazer a
requisição e, caso algum deles não tenha sido passado, sairemos do programa
e mostraremos a forma de usar.
const fileName = process.argv[2];
const quantityOfParagraphs = process.argv[3];
const USAGE = 'USO: node loremipsum.js {nomeArquivo}
{quantidadeParágrafos}';
if (!fileName || !quantityOfParagraphs) {
return console.log(USAGE);
}
O comando return console.log(USAGE) forçará a saída, mostrando na tela a
forma correta de usar. A próxima verificação a ser feita é garantir que o
primeiro argumento (process.argv[2]) seja um número válido e o segundo
argumento possa ser um nome de arquivo, ou seja, rejeitaremos caracteres
especiais. Se não for passado algum desses argumentos, o valor deles será
undefined, que é um dos valores que podemos converter para false no
JavaScript.
Com isso em mente, podemos remover com expressões regulares os
caracteres que não queremos aceitar; logo, nosso programa final ficará assim:
Arquivo final loremipsum.js
#!/usr/bin/env node

/**
* 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.

Figura 2.1 – Filme A new hope, visto no terminal, via TCP.


Vamos construir um pequeno e simples chat TCP com NodeJS, utilizando o
módulo net (https://nodejs.org/api/net.html) nativo do core da plataforma.
#!/usr/bin/env node
const net = require('net')
const chatServer = net.createServer()
const clientList = []
Criamos o servidor com a função net.createServer() e uma variável para conter a
lista de usuários conectados clientList.
const broadcast = (message, client) => {
clientList
.filter(item => item !== client)
.forEach(item => item.write(message))
}
Depois declaramos a função broadcast que será responsável por enviar o que
for digitado por qualquer usuário aos demais que também estão conectados.
Utilizei um filter para não duplicar a mensagem para quem acabou de enviar.
chatServer.on('connection', (client) => {
client.write('Hi guest' + '!\n')
//..
})
Agora definimos o que irá acontecer quando um usuário conectar, ou seja, o
evento on('connection') for disparado. Estamos escrevendo uma mensagem e
pulando uma linha com o \n.
clientList.push(client)
//...
client.on('end', () => {
console.log('client end', clientList.indexOf(client))
clientList.splice(clientList.indexOf(client), 1)
})
Sempre que um novo usuário client conecta, guardamos uma referência dele
no array clientList; caso ele desconecte, ou seja, quando o evento on(‘end') for
recebido pelo servidor tcp, temos que removê-lo.
client.on('data', (data) => broadcast(data, client))
Quando algum usuário digitar algo, logo o evento on('data') será disparado,
podemos chamar a função broadcast para transmitir para todos que estiverem
conectados.
chatServer.listen(9000)
O último passo é dizer que o nosso servidor TCP estará ouvindo conexões na
porta 9000. O programa completo fica assim:
Arquivo chat-tcp.js
#!/usr/bin/env node
const net = require('net')
const chatServer = net.createServer()
const clientList = []
const broadcast = (message, client) => {
clientList
.filter(item => item !== client)
.forEach(item => item.write(message))
}
chatServer.on('connection', (client) => {
client.write('Hi guest' + '!\n')
clientList.push(client)
client.on('data', (data) => broadcast(data, client))
client.on('end', () => {
console.log('client end', clientList.indexOf(client))
clientList.splice(clientList.indexOf(client), 1)
})
client.on('error', (err) => console.log(err))
})
chatServer.listen(9000)
Para iniciar nosso servidor TCP, utilizaremos o comando node seguido pelo
nome do arquivo no qual salvamos o código anterior, em uma janela do
terminal.
$ node chat-tcp.js
Repare que o cursor no terminal ficará aberto, em estado de espera. Isso
porque abrimos um processo que é um ouvinte, aguardando que novos
clientes se conectem a ele.
Para conversar nesse chat, basta conectar no IP de rede da máquina que está
com o programa do servidor em execução, na porta 9000, como definimos no
código. Em uma outra aba do seu terminal, conecte-se como cliente, com o
comando telnet.
$ telnet localhost 9000
Trying ::1...
Connected to localhost.
Escape character is '^]'.
Hi guest!
Alguém que estiver na mesma rede local que você poderá se conectar ao seu
servidor TCP, informando o seu IP local:
$ telnet 192.168.1.4 9000
Trying 192.168.1.4...
Connected to wbruno-macbook.home.
Escape character is '^]'.
Hi guest!
Feito isso, vocês podem conversar num chat TCP!
Note que, se o servidor for parado com Ctrl +C, os dois clientes serão
desconectados na hora.
$ telnet localhost 9000
Trying ::1...
Connected to localhost.
Escape character is '^]'.
Hi guest!
Connection closed by foreign host.

2.4 Criando um servidor HTTP


Bem parecido com o chat TCP, um servidor HTTP também é um processo
que fica aberto aguardando conexões. Utilizaremos o módulo http
(https://nodejs.org/api/http.html) do core da plataforma para construir um
simples servidor HTTP em NodeJS.
const http = require('http')
const server = http.createServer((request, response) => {
//..
})
Importamos o módulo http e criamos o servidor com http.createServer().
response.writeHead(200, {'Content-Type': 'text/plain'})
response.end('Open the blast doors!\n')
Escrevemos um cabeçalho com o status code 200, o content type de texto, e a
string 'Open the blast doors!\n'.
server.listen(1337)
Colocamos o listener na porta 1337, e está pronto o nosso Hello World em
NodeJS. Fica assim o programa completo:
Arquivo server-http.js
const http = require('http')
const server = http.createServer((request, response) => {
response.writeHead(200, {'Content-Type': 'text/plain'})
response.end('Open the blast doors!\n')
})
server.listen(1337, '127.0.0.1', () => {
console.log('Server running at http://127.0.0.1:1337/')
})
Iniciamos o servidor pelo terminal:
$ node server-http.js
Server running at http://127.0.0.1:1337/
Depois, abrindo um navegador e visitando o endereço http://127.0.0.1:1337/,
veremos a frase 'Open the blast doors!' na página.
Note que, assim como no chat, fizemos o require de um módulo, criamos um
servidor com a função createServer e delegamos a uma porta um listener. Foi a
porta 9000 no TCP, e agora a porta 1337 no HTTP, mas poderia ser qualquer
outro número acima de 1024, que ainda não estivesse em uso.
Atualmente, nosso servidor HTTP responde a mesma string para qualquer
URL. Ao acessar http://localhost:1337/, mostra a frase Open the blast doors!, e
http://localhost:1337/close também mostra Open the blast doors!.

2.4.1 Outros endpoints


Os dois parâmetros da função createServer() são os objetos request e response,
que representam uma requisição HTTP e consistem sempre em um pedido e
uma resposta, respectivamente.
O truque para responder a mais de uma requisição está em identificar o que
foi solicitado e então escrever algo diferente na tela. Para isso, podemos dar
uma olhada na propriedade url do objeto request.
Arquivo server-http.js
const http = require('http')
const server = http.createServer((request, response) => {
response.writeHead(200, {'Content-Type': 'text/plain'})
if (request.url === '/') {
response.end('Open the blast doors!\n')
} else if (request.url === '/close') {
response.end('Close the blast doors!\n')
} else {
response.end('No doors!\n')
}
})

server.listen(1337, '127.0.0.1', () => {


console.log('Server running at http://127.0.0.1:1337/')
})
Derrube o servidor com Ctrl + C e inicie novamente com o comando node
server-http.js. Agora, ao acessar pelo navegador http://localhost:1337, vemos a
frase 'Open the blast doors! '. Ao acessar http://localhost:1337/close, aparece
''Close the blast doors! ', e ao tentar qualquer outra coisa diferente disso,
aparecerá a frase 'No doors!'.
A função require que utilizamos para disponibilizar o módulo http no programa
é uma das poucas funções síncronas do NodeJS. Outra coisa importante a
notar é que o require é cacheado, então não importa quantas vezes você faça
require do mesmo módulo, o core do NodeJS só vai de fato acessar o disco
uma única vez; portanto, tenha cuidado ao tentar alterar algo importado com
require, pois estará alterando a referência do módulo na sua aplicação como
um todo.
Ou, utilizando a coleção Map, para melhorar a manutenibilidade e legibilidade
do código, ficaria assim:
const http = require('http')
const routes = new Map()
routes.set('/', (request, response) => response.end('Open the blast doors!\n'))
routes.set('/close', (request, response) => response.end('Open the blast doors!\n'))
const server = http.createServer((request, response) => {
response.writeHead(200, {'Content-Type': 'text/plain'})
if (!routes.has(request.url))
return response.end('No doors!\n')

return routes.get(request.url)(request, response)


})
server.listen(1337, '127.0.0.1', () => {
console.log('Server running at http://127.0.0.1:1337/')
})

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

2.6 Express Generator


O módulo Express Generator (http://expressjs.com/en/starter/generator.html)
cria uma estrutura inicial bem bacana para começar projetos. Execute o
módulo express-generator com o npx:
$ npx express-generator
Será criada a seguinte estrutura:
public/ – arquivos estáticos como CSS, imagens e JavaScript cliente-side;
routes/ – rotas da aplicação;
views/ – HTMLs renderizados pelo servidor;
app.js – arquivo com as definições do servidor;
package.json – declaração das dependências;
bin/www – listener HTTP.

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.

2.7 Método process.nextTick


Entender como o NodeJS funciona e por que é tão importante programar tudo
de forma assíncrona também é um passo muito importante para colocar uma
aplicação no ar.
Rotinas síncronas são bloqueantes, pois ocupam o processador até estarem
finalizadas. Como o NodeJS é single-thread, se o processo estiver ocupado
realizando alguma rotina síncrona muito demorada, então a sua API ficará
incapaz de responder a novas requisições até que a rotina termine. Em outras
palavras, qualquer coisa que bloquear o Event Loop irá bloquear tudo.
Por esse motivo, o NodeJS não é uma boa escolha para trabalhos que
envolvam processamento pesado, como tratamento de imagens, parser de
arquivos gigantes etc. Desde que este não seja o principal intuito da
aplicação, se você estiver escrevendo uma ferramenta de linha de comando
que redimensione imagens, então tudo bem; mas, se você estiver escrevendo
uma API que deve responder a milhares de requisições simultâneas, então
fazer o NodeJS responder a essas requisições e redimensionar imagens ao
mesmo tempo talvez não seja uma boa ideia.
Em uma situação assim, o ideal seria fazer o NodeJS delegar o trabalho
pesado de CPU para outra rotina, talvez escrita até em outra linguagem, que
poderia ter melhor desempenho em uma situação dessas e deixaria o NodeJS
como o maestro, transferindo para o Event Loop a espera do término do
processamento, sem bloqueio.
Quando precisarmos de algo assim no meio do código, como uma função
recursiva, poderemos utilizar o método process.nextTick()
(https://nodejs.org/api/process.html#process_process_nexttick_callback).
Esse método adia a execução de uma função para o próximo ciclo do Event
Loop, liberando assim o processo principal para receber novas requisições e
enfileirá-las para execução.
Em vez de usar:
var recursiveCompute = function() {
//...
recursiveCompute();
};
recursiveCompute();
faria:
var recursiveCompute = function() {
//...
process.nextTick(recursiveCompute);
};
recursiveCompute();

2.8 Star Wars API


Usaremos a Star Wars API (https://swapi.dev/) para escrever um programa
que, quando invocado, realiza diversas requisições HTTP, interpola o retorno
em um template markdown e escreve o resultado em um arquivo .html.
Olhando o contrato da API:
$ curl -i 'https://swapi.dev/api/people/'
HTTP/2 200
server: nginx/1.16.1
date: Sat, 09 Jan 2021 15:50:48 GMT
content-type: application/json
vary: Accept, Cookie
x-frame-options: SAMEORIGIN
etag: "35e90fa80df5a1200859818a74a65a4e"
allow: GET, HEAD, OPTIONS
strict-transport-security: max-age=15768000

{"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.

Figura 2.2 – Template markdown com visualização do HTML.


Precisamos de um template engine para fazer interpolação das variáveis com
o markdown, para isso usaremos uma feature das template strings.
const engine = (template, ...data) => {
return template.map((s, i) => s + `${data[i] || ''}`).join('')
}
const title = 'Star Wars API'
const count = 82
const items = [{
name: 'Luke Skywalker',
height: '172',
mass: '77',
hair_color: 'blond',
skin_color: 'fair',
eye_color: 'blue',
birth_year: '19BBY',
gender: 'male',
homeworld: 'http://swapi.dev/api/planets/1/',
films: [Array],
species: [],
vehicles: [Array],
starships: [Array],
created: '2014-12-09T13:50:51.644000Z',
edited: '2014-12-20T21:17:56.891000Z',
url: 'http://swapi.dev/api/people/1/'
}]
A função engine fará toda a mágica de que precisamos ao passar uma template
string para ela. É importante não ter tabulação à esquerda na declaração do
template, para não interferir no markdown final gerado.
engine`
# ${title}
Tem ${count} pessoas
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')}
`
O resultado será:
'\n' +
'# Star Wars API\n' +
'\n' +
'Tem 82 pessoas\n' +
'\n' +
'Name | Height | Mass | Hair Color | Skin Color | Eye Color | Birth Year | Gender
|\n' +
'---------------|--------|------|------------|------------|-----------|------------|--------|\n' +
'Luke Skywalker|172|77|blond|fair|blue|19BBY|male|\n'
Com o módulo marked (https://github.com/markedjs/marked) vamos
converter o markdown em HTML.
Arquivo index.js
const axios = require('axios')
const fs = require('fs/promises')
const marked = require("marked")
const engine = (template, ...data) => {
return template.map((s, i) => s + `${data[i] || ''}`).join('')
}
const render = result => {
const title = 'Star Wars API'
const count = result.data.count
const items = result.data.results
const markdown = engine`
# ${title}

Tem ${count} pessoas

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.

Figura 2.3 – Visualização no arquivo HTML.


CAPÍTULO3
REST (Representational State
Transfer)

REST (Representational State Transfer – Transferência de Estado


Representacional) é um design de arquitetura para troca de informações entre
aplicações pela rede. Esse termo foi definido no ano de 2000 por Roy
Fielding
(https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm), que
também havia sido um dos autores da especificação do protocolo HTTP.
Utilizamos REST para desenvolver no protocolo HTTP e, quando seguimos
as restrições descritas pela especificação, podemos dizer que fizemos um web
service RESTful.
Um web service é uma solução de integração e comunicação entre aplicações
diferentes através da internet. Essa arquitetura permite que sistemas
disponibilizem, com segurança, dados para outros consumidores. São
preceitos que um web service deve garantir autenticidade, privacidade e
integridade. Grosso modo, é uma forma de permitir que outras aplicações
façam queries no seu banco de dados, mas apenas as que você permitir e se
aquele cliente tiver permissão de acesso suficiente para poder realizar essa
operação.
Uma API (Application Public Interface) é a interface que expomos ao mundo.
É a forma de deixar que outras aplicações manipulem a nossa aplicação, os
nossos dados, seja editando ou apenas filtrando alguma informação.
Vamos juntar todos esses conceitos e construir um web service com uma API
RESTful.
Para construir uma boa API
(https://blog.apigee.com/detail/restful_api_design), precisamos entender cada
uma das partes e planejar um design que faça sentido para os nossos clientes.

3.1 Exemplos de APIs


Estamos cercados por APIs. Existem APIs por todos os lados. Se você for um
desenvolver web, já deve ter utilizado alguma ao longo da sua carreira.
Geralmente encontramos a documentação dela, que explica o que podemos
fazer se utilizarmos tal URL, se enviarmos tais dados...
• Loripsum, Lorem Ipsum API – http://loripsum.net
• Facebook, Graph API – https://developers.facebook.com/docs/graph-api
• Twitter, REST API – https://dev.twitter.com/rest/public
• YouTube, Data API – https://developers.google.com/youtube/v3/
• GitHub API – https://docs.github.com/en/free-pro-team@latest/rest
O legal de expor uma maneira pública para que outras pessoas implementem
algo com base em nossos serviços é que surgem novas formas de utilizar os
dados. Por exemplo, os logins sociais. Logar com Facebook, Twitter, Apple,
GitHub, LinkedIn só é possível porque essas empresas disponibilizaram um
web service com a API para autenticar um usuário utilizando a base de dados
deles.
Num contexto corporativo, em que nem sempre disponibilizamos uma API
publicamente para outras empresas, também é interessante implementar APIs,
pois, por meio dessa arquitetura, conseguimos desacoplar sistemas.
Conseguimos quebrá-los em sistemas menores, escaláveis e mais fáceis de
serem mantidos, em vez de ter uma única aplicação monolítica, em que uma
alteração em uma regra de negócio pode afetar uma terceira parte que parecia
desconectada.
Com essa separação, temos sistemas diferentes, com códigos-fonte diferentes,
cada um cuidando de uma pequena parte e todos conversando entre si por
meio dos contratos que definimos ao documentar a nossa API, além de
possibilitar uma reutilização de código.
Imagine um e-commerce que tem um aplicativo mobile. Para que os produtos
sejam listados em cada uma das distribuições mobile existentes (iOS,
Android, FirefoxOS, Windows Phone etc.), é necessário que exista uma API
que será consultada pelos aplicativos. E, se o próprio e-commerce utilizar
essa mesma API, aí teremos um completo reúso da funcionalidade, uma
ótima consistência dos dados e apenas um ponto para dar manutenção e
beneficiar diversos clientes.
Na web, um web service que trafega informações via HTTP é composto
basicamente de duas partes: a requisição e a resposta.

3.2 Estrutura da requisição


Uma requisição é um pedido que fazemos a um web service. O protocolo
HTTP é baseado em pedido e resposta. Quando um navegador acessa um site,
ele está pedindo um conteúdo para o servidor daquele site. Esse conteúdo que
vem em forma de HTML é a resposta do servidor.
Atualmente, utilizamos o protocolo HTTP 1.1. Nele temos que uma
requisição gera uma resposta. Porém, essa requisição pode ser bem detalhada
e especificar muitas coisas, como qual tipo de mídia queremos como retorno,
quais tipos de dados e em qual quantidade etc.
Veja a estrutura de uma requisição:
• Endpoint – é o URL, um endereço web. Ex.: http://site.com.br/livros;
• Query – é a query string na URI. Exemplo: ?param=value&param2=value2;
• Recurso – é um caminho. Ex.: http://site.com.br/livros (a palavra livros é
o recurso);
• Parâmetros – são variáveis enviadas na URI. Ex.:
http://site.com.br/livros/1 (o número 1 é o parâmetro);
• Cabeçalho – são dados adicionais enviados na requisição. Ex.: tipo de
mídia que aceitamos como retorno, token para autenticação, cookies etc.;
• Método – é o tipo de requisição, chamado também de verbo. Os métodos
existentes no HTTP são: OPTIONS, GET, HEAD, POST, PUT, PATCH,
DELETE, TRACE e CONNECT;
• Dado – é o corpo da requisição. Ex.: quando enviamos um formulário via
POST, os dados nos inputs são o corpo da requisição.
Endpoint
Quando ouvir alguém falando sobre endpoints, entenda como a URL de um
web service. É o caminho web até alguma coisa. Aquele endereço que
digitamos no browser, por exemplo. Um endpoint é composto de três
partes: query, recurso e parâmetro. Um endpoint também pode ser chamado
de rota.

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.

3.3 Estrutura da resposta


A resposta é o retorno, ou seja, é o resultado de uma requisição. Veja a
estrutura de uma resposta:
• Status code – é um número de 100 a 599. Ex.: 404 para página não
encontrada.
• Dado – é o corpo do retorno. Ex.: ao pedir por um HTML, o HTML é o
corpo do retorno.
• Cabeçalho – são informações extras enviadas pelo servidor. Ex.: tempo
de expiração de um recurso.

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.

3.4 Restrições do REST


O REST foi definido com base em seis restrições, que são: client-server,
stateless, cacheable, uniform interface, layered system e code-on-demand.

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.

3.5 Testando a requisição com curl


Podemos utilizar os comandos curl apresentados a seguir para fazer
requisições HTTP direto do terminal. O curl tem algumas flags para que
indiquemos corretamente as partes da requisição. Com -H informamos algum
cabeçalho, e com –d informamos o dado a ser enviado.
O comando curl não vem habilitado por padrão nas novas versões do
Windows; portanto, para os exemplos a seguir, lembre-se de instalá-lo:
https://curl.haxx.se/dlwiz/?type=bin.

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

PUT ou PATCH (update)


Atualiza as informações de um item.
$ curl -H "Content-Type: application/json" \
-H "X-HTTP-Method-Override: PUT" -d '{"phrase":"May the Force be with
you."}' \
http://127.0.0.1:3000/quotes/55061dc648ccdc491c6b2b61

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.

3.6 Testando a requisição com o Postman ou


Insomnia
O Postman (https://www.getpostman.com) é um programa visual para fazer
requisições HTTP. Podemos utilizá-lo em vez do comando curl para testar as
rotas da nossa API, conforme vemos na Figura 3.1.
Figura 3.1 – Interface do Postman.
Para o servidor que iremos escrever, deixe sempre selecionado raw, com a
opção JSON. Se quisermos aceitar os outros tipos de dados na requisição,
precisamos implementar outros middlewares no Express.
O Insomnia (https://insomnia.rest/download/) é outra opção para fazer
requisições HTTP e até documentar a sua API para a sua equipe. Na Figura
3.2 vemos a interface do programa.
Leitura complementar: https://github.com/Gutem/http-api-design/.
No repositório GitHub do livro, coloquei a collection do Insomnia:
https://github.com/wbruno/livro-
nodejs/blob/main/resources/Insomnia_troopers.json e collection do Postman:
https://github.com/wbruno/livro-
nodejs/blob/main/resources/Postman_troopers.json da API que
desenvolveremos nos próximos capítulos.
Você pode importar esse arquivo .json.
Figura 3.2 – Interface do Insomnia.

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

Com NodeJS, é possível trabalhar com qualquer banco de dados existente,


basta procurar um módulo correspondente, por exemplo:
• Oracledb (https://github.com/oracle/node-oracledb);
• MySQL (https://github.com/mysqljs/mysql);
• Postgres (https://github.com/brianc/node-postgres);
• SQL Server (https://github.com/tediousjs/node-mssql);
• MongoDB (https://github.com/mongoist/mongoist);
• Redis (https://github.com/NodeRedis/node_redis).
Esses são conectores simples, mas, caso você queira um ORM completo, há
outras opções bem interessantes, como o Sequelize
(https://sequelize.org/master/) e o Mongoose (https://mongoosejs.com/docs/).
Vou apresentar brevemente três modelos diferentes de bancos de dados. O
Postgres, como banco de dados relacional que utiliza SQL, o MongoDB, que
é orientado a documentos, e o Redis, que é outro exemplo de NoSQL.
Neste capítulo construiremos um cadastro dos soldados de elite do Império
Galáctico, os stormtroopers. Uma entidade tem atributos que são
características que queremos registrar. Um atributo não deve conter um grupo
de informações, deve ser conciso. Não existem entidades com menos de dois
atributos; logo, cada entidade é, em si, um grupo de atributos.
É interessante que cadastremos as seguintes informações sobre um
stormtrooper: o nome, o apelido, a divisão (infantaria, operações especiais,
legião, batalhão etc.) e a patente (sargento, capitão, comandante, tenente etc.),
esses são os atributos, e o soldado em si é a nossa entidade.
4.1 Postgres
O Postgres é um SGBD (Sistema de Gerenciamento de Banco de Dados) que
utiliza a linguagem SQL para prover acesso aos dados nele armazenados. É
um banco de dados relacional e, portanto, é uma boa prática aplicar as
Formas Normais quando você estiver modelando suas entidades.
As tabelas são os locais onde os dados são armazenados. As colunas são, por
assim dizer, os atributos de uma entidade, e cada linha é uma instância de
uma entidade, no nosso caso, um soldado.
A normalização é um processo em que, por meio de regras aplicadas às
entidades, evitamos redundância de dados, mistura de propriedades, e
tornamos o nosso modelo escalável.
• Primeira forma normal (1FN) – uma tabela está na 1FN se – e somente
se – todas as colunas forem atômicas, ou seja, não tiver atributos
multivalorados.
• Segunda forma normal (2FN) – uma relação está na 2FN se – e
somente se – estiver na 1FN e cada atributo não chave for dependente da
chave primária inteira, isto é, cada atributo não chave não poderá ser
dependente de apenas parte da chave.
• Terceira forma normal (3FN) – uma relação R está na 3FN se estiver
na 2FN e cada atributo não chave de R não tiver dependência transitiva
para cada chave candidata de R.
• Quarta forma normal (4FN) – uma tabela está na 4FN se – e somente
se – estiver na 3FN e não possui redundâncias, em que campos que
podem ser calculados a partir de outros não devem ser persistidos.
• Quinta forma normal (5FN) – estando na 4FN, uma entidade chega à
5FN se não for possível reconstruir as informações originais a partir de
registros menores, resultado da decomposição de um registro principal.
Diz respeito à dependência de junção, mas é raramente utilizada.
Devido a isso, geralmente acabamos com diversas tabelas para representar
uma única entidade, caso ela seja complexa ou tenha muitas propriedades.
Alguns fatos importantes a observar sobre o nome do banco de dados e das
tabelas ou views são, geralmente:
• o nome do banco reflete o nome do projeto;
• não utilizamos hifens, logo, separamos palavras com underlines;
• nomes em inglês;
• o nome de tabelas que representam entidades será no plural;
• todas as palavras são em letra minúscula (pois estamos utilizando
Postgres; se fosse Oracle, seriam todas em maiúsculas).
Utilizarei o terminal para gerenciar o banco de dados, mas você pode utilizar
algum cliente com interface gráfica, como o DBeaver, Squirrel, SQL
Developer, pgAdmin ou algum outro.
$ psql -d postgres
psql (13.1)
Type "help" for help.
postgres=#
Vamos criar o database livro_nodejs e informar que iremos trabalhar com ele:
postgres=# create database livro_nodejs;
CREATE DATABASE
postgres=# \c livro_nodejs
You are now connected to database "livro_nodejs" as user "wbruno".
livro_nodejs=#

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:

Figura 4.2 – Foto dos dados no banco de dados.


Se você se perdeu um pouco na modelagem, não se preocupe. Esse exemplo
com SQL foi para ilustrar como é complexo construir relacionamentos. Para
seguir as Formas Normais, precisamos criar diversas tabelas e utilizar vários
JOINs até representar corretamente a nossa entidade.
Caso você queira reiniciar os dados, para inserir novamente, basta rodar
alguns truncates:
truncate stormtroopers restart identity cascade;
truncate divisions restart identity cascade;
truncate patents restart identity cascade;
truncate stormtrooper_division restart identity cascade;
No GitHub do livro, há os scripts completos para criação do banco de dados:
https://github.com/wbruno/livro-nodejs/blob/main/resources/postgres.sql.

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.

Figura 4.3 – Comparação entre conceitos de um banco relacional e o


MongoDB.
Sobre convenções, padrões no nome do banco de dados e coleções:
• o nome do database representa o nome do projeto;
• podemos utilizar hifens ou underlines no nome do banco, mas nas
collections evitamos hifens;
• nomes em inglês;
• as coleções serão nomeadas no plural;
• utilizamos somente letras minúsculas;
• não utilize os caracteres arroba (@), dois pontos (:) ou algum outro
caractere especial que possa confundir a string de conexão na senha, no
nome do banco ou no usuário do MongoDB;
• não crie uma coleção com o mesmo nome do banco de dados.

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

Veremos neste capítulo como fazer uma API em NodeJS, utilizaremos o


módulo ExpressJS e posteriormente o fastify, mas os conceitos apresentados
podem ser usados de forma geral com qualquer outro framework.
Uma API (Application Programing Interface) é um conjunto de rotinas e
padrões estabelecidos por um software para a utilização das suas
funcionalidades por aplicativos que não pretendem envolver-se em detalhes
da implementação, mas apenas usar seus serviços.

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.

Figura 5.1 – git diff mostrando como utilizar ES6 modules.


Note que, ao mudar para "type": "module", não é mais possível utilizar require:
const express = require('express')
^
ReferenceError: require is not defined
O valor padrão do type é commonjs
(https://nodejs.org/docs/latest/api/modules.html). CommonJS foi o sistema
de módulos adotado no NodeJS desde o início, em que usamos require e
module.exports.

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.

5.2.1 Entendendo a utilidade


Usamos middlewares para tratar os requests que o servidor irá receber.

Tratando página não encontrada


A forma correta de tratar um erro é declarar um objeto Error e enviar para a
função next(err) com um middleware após já ter declarado todas as rotas da
aplicação. Por padrão, o ExpressJS já declara como as rotas não declaradas e
os erros serão respondidos. Ao acessar: http://localhost:3000/nao-existe, é
retornado: Cannot GET /nao-existe, com status code 404.
Veja como ficará o nosso server/app.js com a customização de rota não
encontrada:
Arquivo server/app.js
import express from 'express'
const app = express()
app.get('/', (req, res) => {
res.send('Ola s')
})
app.use((request, response, next) => {
var err = new Error('Not Found')
err.status = 404
next(err)
})
app.listen(3000)
Feito isso, a resposta já muda para:
Error: Not Found
at file:///Users/wbruno/Sites/wbruno/livro/capitulo_5/5.1/server/app.js:9:13
at Layer.handle [as handle_request] …
É importante que o código de tratamento de erros seja declarado depois da
criação de todas as rotas da aplicação, pois, se alguma requisição não
coincidir com as declaradas, podemos seguramente assumir que o nosso
servidor não tem aquele recurso e disparar um status code 404 para o cliente.
var err = new Error('Not Found')
err.status = 404
Caso seja algum outro tipo de erro, tratamos de apenas renderizar uma página
HTML ou mostrar um JSON simplificado com o motivo de ter falhado, mas
sem vazar para o cliente todo o stack trace do erro (detalhes que podem
comprometer a segurança da aplicação, como, por exemplo, o caminho no
servidor em que os nossos arquivos estão).
Agora só falta customizar o middleware de erros, que deve sempre ser o
último da cadeia e recebe quatro argumentos: err, request, response e next.
app.use((err, request, response, next) => {
if (err.status !== 404) console.log(err.stack)
response.status(err.status || 500).json({ err: err.message })
})
Note que eu coloquei o err.stack para ser mostrado no console.log(). Dessa
forma, irá aparecer a pilha de execução no terminal em que você subiu a
aplicação quando algum erro for capturado por esse middleware do
ExpressJS, facilitando a detecção do problema enquanto estivermos
desenvolvendo.
Dessa forma, evitamos o vazamento das execuções do script (stack trace)
para o usuário, já que podemos renderizar um HTML de erro 500, um JSON
ou uma mensagem amigável do que ocorreu sem expor informações que
comprometeriam a segurança da aplicaçã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('')
})

CORS (Cross-origin resource sharding)


A sigla CORS significa compartilhamento de recursos entre origens
diferentes, que é o fato de uma aplicação web solicitar informações de um
domínio ou subdomínio diferente do seu próprio. O comportamento padrão
dos navegadores web atuais é bloquear esse tipo de requisição por motivos de
segurança, impedindo que a aplicação seja afetada pela resposta não
confiável.
Se quisermos permitir o uso do recurso, liberamos o cabeçalho CORS na
nossa API, para que qualquer cliente consiga utilizar, de qualquer domínio,
incluindo alguns cabeçalhos nas respostas HTTP. Para isso, utilizaremos a
seguinte configuração no ExpressJS antes da declaração das rotas:
app.use((request, response, next) => {
response.header('Access-Control-Allow-Origin', '*')
response.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-
Type, Accept')
next()
})
É importante notar que o asterisco libera para todas as origens. Outra forma é
utilizar o pacote cors (https://www.npmjs.com/package/cors), que faz a mesma
coisa, porém encapsula essa complexidade.
const cors = require('cors')
app.use(cors())
Ou com ES6 modules:
import cors from 'cors'
app.use(cors())
Existem diversas configurações disponíveis, como liberar somente para certo
domínio, somente alguns métodos HTTP etc.
O módulo cors é um exemplo de middlewares de terceiros. Ainda existem
diversos outros que podemos adicionar com a função app.use() e, assim, por
meio dessa soma de pequenos módulos, construir a nossa aplicação.
O lado bom é que não temos que nos preocupar em codificar esses
comportamentos, pois já existem módulos npm da comunidade que resolvem
diversos desses problemas.
Um middleware embutido (built-in) já faz parte do core do framework, como,
por exemplo, para servir arquivos estáticos:
app.use(express.static(path.join(__dirname, 'public')))
Ou com ES6 modules, já que a variável global __dirname não existe,
precisamos de um pequeno hack:
import express from 'express'
import { dirname } from 'path'
import { fileURLToPath } from 'url'
const app = express()
const __dirname = dirname(fileURLToPath(import.meta.url));
app.use(express.static(__dirname + '/../public'))
export default app
E os middlewares de rota são aqueles em que passamos um caminho e um
objeto router como argumentos.
app.use('/', require('./routes'));
ou:
app.use('/api', require('./api'));
Conseguimos limitar a cadeia de atuação, de acordo com a ordem de
definição, caminho (rota ou path) e método HTTP. Imagine alguns
middlewares mid1, mid2, mid3 parecidos com isso:
const mid1 = (request, response, next) => {
//...
next()
}
Podemos combiná-los de diversas formas:
app.use(mid1, mid2, mid3)
O código anterior executará o mid1, depois o mid2 e por último o mid3 se cada
um deles não passar nenhum argumento para função next(). Caso o mid1, por
exemplo, invoque next(err), com um objeto Error, a cadeia de middlewares é
quebrada, ou seja, mid2 e mid3 nunca serão invocados, e a execução pula para
o middleware de erro (aquele com quatro argumentos).
app.get('/weapons, mid3)
só executará o mid3 se o request for um GET na rota /weapons.
app.use(mid1)
app.put('/weapons', mid2, mid3)
app.post('/weapons', mid3)
Com a declaração acima, o mid1 sempre será executado, tanto antes do PUT
quanto antes do POST.

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.2.2 Tipos de resposta


Uma das vantagens de trabalhar com NodeJS é que estamos escrevendo
JavaScript; então, para fazer uma API que retorne um JSON, tudo o que
temos a fazer é escrever esse JSON. Não precisamos fazer um mapper, parser
ou converter um array para JSON. Apenas escrevemos o JSON (JavaScript
Object Notation) que queremos diretamente. Se quisermos mostrar um JSON
na tela, poderemos trocar o res.send() por res.json():
res.json({ 'name': 'William Bruno', 'email': '[email protected]' });
Junto à resposta, podemos enviar o status code:
res.status(201);
res.json({ 'name': 'William Bruno', 'email': '[email protected]' });
É importante que o status seja enviado antes da resposta (do send() ou do
json()) e invoquemos uma única vez por requisição uma das seguintes
funções: .end(), .send() ou .json(). Caso contrário, um erro de cabeçalho já
enviado será retornado no console do NodeJS para nós.
Arquivo app.js
import express from 'express'
const app = express()
app.get('/', (request, response) => {
response.status(201)
response.json({ 'name': 'William Bruno', 'email': '[email protected]' })
})
app.listen(3000)
É possível identificar qual o tipo de mídia que o cliente quer receber e então
retornar o formato mais adequado que tivermos à mão. Por exemplo, se o
cabeçalho accept for text/plain, o retorno deverá ser um texto puro.
$ curl -H "Accept: text/plain" http://localhost:3000
name; email
William Bruno; [email protected]
Caso seja application/json, deverá retornar um JSON.
$ curl -H "Accept: application/json" http://localhost:3000
{"name":"William Bruno","email":"[email protected]"}
Para fazer isso, precisamos apenas identificar o que o cliente espera.
const data = { 'name': 'William Bruno', 'email': '[email protected]' };
app.get('/', (request, response) => {
if (request.accepts('text')) {
const keys = Object.keys(data)
const values = Object.values(data)
response.write(`${keys.join('; ')}\n`)
response.write(`${values.join('; ')}\n`)
response.end()
} else {
response.json(data)
}
})
Ou utilizando o método response.format do Express:
app.get('/', (request, response) => {
response.format({
text: () => {
const keys = Object.keys(data)
const values = Object.values(data)
response.write(`${keys.join('; ')}\n`)
response.write(`${values.join('; ')}\n`)
response.end()
},
default: () => {
response.json(data)
}
})
})

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 Melhorando o listener do servidor


Extrairemos o server listener para um arquivo especializado.
No arquivo server/app, troque:
app.listen(3000)
por:
export default app
Agora, o seu arquivo server/app.js deve estar assim:
Arquivo server/app.js
import express from 'express'
const app = express()
app.get('/', (request, response) => {
response.json({ 'name': 'William Bruno', 'email': '[email protected]' })
})
export default app
Crie o arquivo server/bin/www.js com o listener que antes estava no server/app.js
e o import do módulo app:
#!/usr/bin/env node
import app from '../app.js'
import debug from 'debug'
const log = debug('livro_nodejs:www')
app.listen(3000, () => log('server started'))
E depois troque server/app.js por server/bin/www.js no package.json:
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": {
"express": "^4.17.1"
}
}
Vamos parar o serviço com Ctrl + C e, por ter trocado o scripts.dev no
package.json, continuaremos a iniciar o servidor com:
$ npm run dev
[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

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
})

5.4.3 HTTP keepalive


Da mesma forma que cada request feito no NodeJS faz resolução de DNS,
independentemente se já fez anteriormente, uma nova conexão HTTP é
aberta para cada request. Seja usando Axios, request, fetch, enfim, qualquer
biblioteca do npm irá, por baixo dos panos, utilizar o modulo http do core da
linguagem e, por padrão, o NodeJS abre uma nova conexão HTTP para cada
request (https://nodejs.org/api/http.html#http_new_agent_options).
Faz sentido manter as conexões abertas e reutilizar; para isso, basta declarar
as linhas a seguir também no arquivo server/bin/www:
import http from 'http'
import https from 'https'
http.globalAgent.keepAlive = true
https.globalAgent.keepAlive = true
Com isso, uma vez estabelecida uma conexão HTTP, ela ficará disponível
num pool para ser reutilizada num próximo request, diminuindo o tempo de
resposta como um todo.

5.5 API Stormtroopers


Vamos utilizar a seguinte estrutura de arquivos para organizar a aplicação:
$ mkdir public views
$ mkdir -p server/bin server/config server/controller server/repository server/routes
$ mkdir -p tests/unit tests/integration

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.

.eslintrc.json, .nvmrc, .gitignore, .travis.yml, .vscode etc.


Na raiz do projeto é comum haver diversos arquivos ocultos, que são
aqueles arquivos sem um nome, apenas uma extensão, ou seja, .algumacoisa.
São arquivos de configuração de softwares e sistemas de terceiros.

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"
]
}

Figura 5.2 – Interface do Insomnia com request de criação.


Temos do lado direito da Figura 5.2, onde está 201 Created, o resultado do
que foi inserido no MongoDB.
O controller do PUT por id fica assim:
updateById(request, response, next) {
repository.updateById(request.params.id, request.body)
.then(result => response.json(result))
.catch(next)
},
E o repository:
updateById(id, data) {
return db.stormtroopers.update({ _id: mongoist.ObjectId(id) }, { $set: data })
},
Testando com curl, temos:
$ curl -X PUT 'http://localhost:3000/troopers/5ff0710b156a1f6e82180a49' -H 'content-
type: application/json' -d '{"patent": "Soldier"}'
{"n":1,"nModified":1,"ok":1}
Vemos na Figura 5.3 como fica a requisição de atualização:
Figura 5.3 – Interface do Insomnia com requisição de atualização.
E agora fazendo um GET por esse id para conferir:
$ curl 'http://localhost:3000/troopers/5ff0710b156a1f6e82180a49'
{"_id":"5ff0710b156a1f6e82180a49","name":"TK-
132137","patent":"Soldier","divisions":["501st legion"]}
Por último, implementando o método DELETE, retornaremos 204, que é o
status code mais apropriado.
deleteById(request, response, next) {
repository.deleteById(request.params.id)
.then(_ => response.sendStatus(204))
.catch(next)
}
E o repository:
deleteById(id) {
return db.stormtroopers.remove({ _id: mongoist.ObjectId(id) })
},
Executando no curl:
$ curl -X DELETE 'http://localhost:3000/troopers/5ff0803f324d405c6ca8fa4e' --head
HTTP/1.1 204 No Content
Revisando o controller:
Arquivo server/controller/Stormtrooper.js
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) {
repository.byId(request.params.id)
.then(handleNotFound)
.then(result => response.json(result))
.catch(next)
},
create(request, response, next) {
repository.create(request.body)
.then(result => response.status(201).json(result))
.catch(next)
},
updateById(request, response, next) {
repository.updateById(request.params.id, request.body)
.then(result => response.json(result))
.catch(next)
},
deleteById(request, response, next) {
repository.deleteById(request.params.id)
.then(_ => response.sendStatus(204))
.catch(next)
}
}
export default Stormtrooper
O nosso repository:
Arquivo server/repository/Stormtrooper.js
import mongoist from 'mongoist'
import db from '../config/mongoist.js'
const Stormtrooper = {
list() {
const query = {}
return db.stormtroopers.find(query)
},
byId(id) {
return db.stormtroopers.findOne({ _id: mongoist.ObjectId(id) })
},
create(data) {
return db.stormtroopers.insert(data)
},
updateById(id, data) {
return db.stormtroopers.update({ _id: mongoist.ObjectId(id) }, { $set: data })
},
deleteById(id) {
return db.stormtroopers.remove({ _id: mongoist.ObjectId(id) })
},
}
export default Stormtrooper
Uma evolução necessária para o repository que acabamos de criar é filtrar
quais campos vamos aceitar repassar para a base de dados. Senão, um cliente
da API poderia até sobrescrever a geração _id, ou criar novos atributos, e
com certeza não queremos isso.
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 } })
},
e o arquivo de rotas:
Arquivo server/routes/trooper.js
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
Esta é a collection do Insomnia: https://github.com/wbruno/livro-
nodejs/blob/main/resources/Insomnia_troopers.json, e esta é a do Postman:
https://github.com/wbruno/livro-
nodejs/blob/main/resources/Postman_troopers.json.
Você pode importá-las para executar os mesmos testes de rotas que fiz no
arquivo server/routes/trooper.js.

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"}

5.6.2 JSON Web Token


Outra forma de autenticação é via JSON Web token. Em vez de o cliente
enviar as credenciais usuário e senha a cada request, podemos permitir que
ele troque essas informações por um token, que é uma forma mais segura,
pois não trafega a senha pela web a cada requisição. Um token no formato
JWT (http://jwt.io) é uma string codificada que conterá as informações
necessárias para o servidor validar a requisição.
O módulo jwt-simple (https://github.com/hokaccha/node-jwt-simple) provê
uma interface de uso muito legal para esse tipo de autenticação. Instale-o:
$ npm install jwt-simple --save
Instale também o módulo moment (http://momentjs.com), que é uma
biblioteca para trabalhar com datas no JavaScript.
$ npm install moment --save
Adicionaremos uma configuração para que a aplicação tenha um salt que
impeça que outras pessoas decodifiquem o token.
Arquivo config/default.json
{
"mongo": {
"uri": "mongodb://localhost:27017/livro_nodejs"
},
"jwtTokenSecret": "Death Star"
}
O usuário irá se autenticar em uma URL, receber um token válido e, nas
próximas requisições, enviar esse token para que a API saiba que pode
permitir que o usuário tenha acesso aos conteúdos. Criaremos, para isso, uma
rota /login, por meio da qual o cliente irá enviar um usuário e uma senha, que
podemos validar no banco de dados ou em algum outro sistema de login, e
depois de verificado devolveremos o token.
Arquivo server/routes/index.js
import { Router } from 'express'
import trooperRoutes from './trooper.js'
const routes = new Router()
routes.get('/', (req, res) => res.send('Ola s'))
routes.post('/login', (request, response, next) => {
const { username, password } = request.body
})
routes.use('/troopers', trooperRoutes)
export default routes
O nosso usuário para o JWT também será rebels e a senha 1138. Retornaremos
o token:
const token = jwt.encode({
user: username,
exp: moment().add(7, 'days').valueOf()
}, config.get('jwtTokenSecret'))
return response.json({ token })
Caso não sejam informados corretamente esses dados, retornaremos um “401
Unauthorized”:
next(createError(401, 'Unauthorized'))

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: Successfully generated boilerplate for template: "aws-nodejs"

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 };
};

5.9 JSON Schema


O projeto JSON Schema (http://json-schema.org) permite descrever e validar
documentos JSON, além de existirem diversas ferramentas que entendem
essa definição e podem gerar documentações para nossas APIs com base no
JSON Schema.
Dado um stormtrooper:
{
"_id": "5ff30c2e7952ec31de6b8e18",
"name": "CC-1010.",
"nickname": "Fox",
"patent": "Major",
"divisions": ["501st Legion", "Coruscant Guard"]
}
Por exemplo, para descrever o campo name, informando o tipo de dado, para
que serve, valor padrão e exemplo do que aceita:
"name": {
"$id": "#/properties/name",
"type": "string",
"title": "Nome do soldado",
"description": "A tag de identificação",
"default": "",
"examples": [
"CC-1010."
]
},
Fica bem fácil para alguém que vá consumir nossa API entender o que deve
informar em cada campo. Usei o https://jsonschema.net para gerar uma
versão inicial do JSON Schema, então posso editar o que precisar depois.
Na Figura 5.4 visualizamos a interface do site jsonschema.net.
Figura 5.4 – Interface do JSON Schema.
Ao colar um JSON de exemplo no lado esquerdo e clicar no botão Submit, é
gerado, por inferência, o JSON Schema correspondente. Entre no GitHub do
livro para ver o JSON Schema completo: https://github.com/wbruno/livro-
nodejs/blob/main/capitulo_5/5.9/json-shema.json.

1 Raiz do projeto: o mesmo nível de diretório do arquivo package.json.


2 req ou res – neste livro não utilizarei essas abreviações, para facilitar as explicações.
3 Round robin: algoritmo de distribuição de carga sem prioridade, em partes iguais de
maneira circular.
6
CAPÍTULO
FrontEnd

Nem sempre devolver arquivos diretamente do disco é suficiente. Precisamos


também mostrar informações, sejam elas em texto puro ou formatadas em
JSON, HTML ou XML.

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))
})

6.1 Arquivos estáticos


Apesar de ser possível, não é recomendado entregar arquivos totalmente
estáticos do NodeJS, pois queremos que a nossa linguagem server-side se
ocupe mais em renderizar informações dinâmicas do que em entregar assets.
Por simplicidade e para projetos simples, fica aqui o exemplo. Iniciaremos
uma nova aplicação neste capítulo, para isso utilize o comando npm init para
iniciar o projeto e instale o módulo ExpressJS como dependência:
$ npm init --yes
$ npm install express --save
Criaremos algumas pastas:
$ mkdir -p server/bin views public
Definiremos o uso de ES6 módulos (export/import) e o scripts.dev para usar
Nodemon.
Arquivo package.json
{
"name": "6.1",
"type": "module",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon server/bin/www.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "William Bruno <[email protected]> (http://wbruno.com.br)",
"license": "ISC",
"dependencies": {
"debug": "4.3.1",
"express": "4.17.1"
}
}

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 Client side


Veremos aqui como consumir a API http://localhost:3000/troopers no
frontend, com JavaScript cliente side, por meio de arquivos estáticos (não
processados no servidor). Para isso, temos que liberar o CORS na API,
colocando o cabeçalho:
app.use((request, response, next) => {
response.header('Access-Control-Allow-Origin', '*')
response.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-
Type, Accept')
next()
})

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>

Figura 6.1 – Console do Safari aberto com o HTML renderizado.

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.

Figura 6.2 – Tela inicial criada pelo CRA.


Apenas editando o arquivo src/App.js, com alguns dados de mock (a constante
troopers), e definindo um pouco de JSX (o trecho HTML dentro do arquivo
.js), já é possível imprimir a nossa tabela:
import './App.css';
function App() {
const troopers = [{
"_id": "5ff30c2e7952ec31de6b8e1a",
"name": "CT-27-5555",
"nickname": "Fives",
"patent": "Soldier"
},
{
"_id": "5ff30c2e7952ec31de6b8e18",
"name": "CC-2224",
"nickname": "Cody",
"patent": "Commander"
}
]
return (
<>
<h1>Stormtroopers</h1>
<table id="target">
<thead>
<tr>
<th>ID</th><th>Name</th><th>Patent</th>
</tr>
</thead>
<tbody>
{
troopers.map(trooper => {
return (
<tr key={trooper._id}>
<td>{trooper._id}</td>
<td>{trooper.name}</td>
<td>{trooper.patent}</td>
</tr>
)
})
}
</tbody>
</table>
</>
);
}

export default App;


Estamos usando chaves {} para escrever uma expressão JavaScript dentro do
JSX, na qual escrevemos um loop .map pelo array de troopers, e retornamos
outro trecho de JSX com as tags HTML <tr> e <td> já interpoladas com os
dados. Uma primeira refatoração para melhorar a legibilidade é criar outros
componentes menores, como THead, TBody e TLine:
import './App.css'
function THead(props) {
return (
<thead>
<tr>
{ props.items.map((item,i) => <th key={i}>{item}</th>) }
</tr>
</thead>
)
}
function TBody(props) {
return (
<tbody>
{
props.items.map(item => {
return <TLine key={item._id} item={item} />
})
}
</tbody>
)
}
function TLine(props) {
return (
<tr>
<td>{props.item._id}</td>
<td>{props.item.name}</td>
<td>{props.item.patent}</td>
</tr>
)
}
function App() {
const troopers = [{
"_id": "5ff30c2e7952ec31de6b8e1a",
"name": "CT-27-5555",
"nickname": "Fives",
"patent": "Soldier"
},
{
"_id": "5ff30c2e7952ec31de6b8e18",
"name": "CC-2224",
"nickname": "Cody",
"patent": "Commander"
}
]
return (
<>
<h1>Stormtroopers</h1>
<table id="target">
<THead items={['ID', 'Name', 'Patent']} />
<TBody items={troopers} />
</table>
</>
)
}
export default App
Também poderiam ter sido escritos em forma de classes, em que, em vez de
receber props como argumento, acessamos o array troopers pela instância
this.props.
class THead extends Component {
constructor(props) {
super(props)
}
render() {
return (
<thead>
<tr>
{ this.props.items.map((item,i) => <th key={i}>{item}</th>) }
</tr>
</thead>
)
}
}
Porém, com a adição de hooks, não é mais necessário usar classes; portanto,
usaremos apenas a sintaxe de funções para criar componentes React.
Removendo o mock e fazendo a requisição para a API, fica assim:
function App() {
const [troopers, setTroopers] = useState([])
useEffect(() => {
fetch('http://localhost:3000/troopers')
.then(response => response.json())
.then(data => setTroopers(data))
}, [])
return (
<>
<h1>Stormtroopers</h1>
<table id="target">
<THead items={['ID', 'Name', 'Patent']} />
<TBody items={troopers} />
</table>
</>
)
}
Este exemplo em ReactJS é minimalista e incompleto, apenas quis mostrar o
início e alguns conceitos básicos.

6.3 Server side


Para construir o HTML no lado do servidor, utilizaremos algum template
engine, que é um software responsável por juntar partes de uma visualização
e injetar dados dentro dessa visualização, trocando variáveis pelos seus
valores reais e interpolando no HTML.
Escolha um template engine que tenha as funções de que você precisa e esteja
sendo mantido.

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.

Figura 6.3 – Mensagem de depreciado do pacote Jade.


Então instale o Pug e salve-o como dependência do projeto.
$ npm install pug --save
e configure no server/app.js:
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
Porém, sua utilização é bem diferente para aqueles que já estão acostumados
com a linguagem HTML. A proposta é que você não tenha que escrever tags
HTML. Para isso, esse módulo utiliza uma sintaxe baseada em indentação,
em que cada nível representa a hierarquia das tags.
Arquivo views/index.pug
doctype html
html(lang="pt-br")
head
meta(charset="UTF-8")
meta(name="viewport" content="width=device-width, initial-scale=1.0")
title=Document
body
h1= title
p= message
É possível usar tags HTML, mas tivemos que renomear as extensões dos
arquivos para .pug.
Arquivo views/loop.pug
if is3D
p Hell yeah!
else
p =(

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.

6.3.4 React Server Side


Usaremos o módulo Express React Views
(https://github.com/reactjs/express-react-views).
$ npm i --save express-react-views react react-dom
Após instalar com a flag --save, ficará assim o package.json:
{
"name": "6.2.4",
"type": "module",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon server/bin/www",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "William Bruno <[email protected]> (http://wbruno.com.br)",
"license": "ISC",
"dependencies": {
"debug": "4.3.1",
"express": "4.17.1",
"express-react-views": "0.11.0",
"react": "17.0.1",
"react-dom": "17.0.1"
}
}

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.

7.1 Criando testes de código


Imagine que tenhamos um arquivo chamado util.js que contenha uma função
que soma os números de um array:
Arquivo util.js
const arraySum = (arr) => {
const sum = 0
return sum
}
module.exports = { arraySum }
Ainda não implementei a função arraySum, só a declarei. O teste é outro
arquivo que compara se, dada uma entrada conhecida, o resultado é a saída
esperada. Então o arquivo tests/util_test.js seria o seguinte:
Arquivo tests/util.test.js
const assert = require('assert')
const util = require('../util')
const sum = util.arraySum([1,2,3])
assert.equal(sum, 6)
Primeiro, eu faço o require do módulo assert
(https://nodejs.org/api/assert.html) nativo do NodeJS, depois faço o require
do meu módulo util, que está na pasta anterior (por isso o ../), invoco a função
util.arraySum() e comparo se o valor retornado pela função é igual ao valor
esperado.
Como não terminamos a implementação, ao executar o teste no terminal,
teremos um erro como resposta:
$ node tests/util.test.js
node:assert:119
throw new AssertionError(obj);
^

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.

7.3 Testes unitários


Os testes unitários são aqueles que testam função por função, ou seja,
unidade por unidade.
Vamos criar testes unitários em cima da aplicação que criamos no Capítulo 5.
$ mkdir -p tests/units
Para isso, vamos extrair o middleware verifyId de dentro do arquivo de rotas:
import { Router } from 'express'
import controller from '../controller/Stormtrooper.js'
import verifyId from '../middleware/verifyId.js'
const trooperRoutes = new Router()
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
para um arquivo separado:
Arquivo server/middleware/verifyId.js
import createError from 'http-errors'
const verifyId = (request, response, next) => {
const id = request.params.id
if (!/^[0-9a-f]{24}$/.test(id)) {
return next(createError(422, 'invalid id'))
}
next()
}
export default verifyId
Assim podemos criar um teste unitário diretamente para esse middleware,
deixando os testes unitários ao lado dos arquivos que estão testando, para
aproveitar a estrutura de diretórios existente.
$ ll server/middleware/
total 16
-rw-r--r-- 1 wbruno staff 240B Jan 5 10:56 verifyId.js
-rw-r--r-- 1 wbruno staff 776B Jan 5 11:40 verifyId.test.js
E incluiremos o sufixo .test.js:
Arquivo server/middleware/verifyId.test.js
import verifyId from './verifyId.js'
let request
let response
let next
beforeEach(() => {
request = {}
response = {}
next = () => {}
})
describe('#verifyId', () => {
it('invalid id', () => {
request.params = { id: '5ff' }
verifyId(request, response, next)
})
it('valid id', () => {
request.params = { id: '5ff30c2e7952ec31de6b8e1a' }
verifyId(request, response, next)
})
})
Como estamos usando ES6 modules, precisamos informar para o Jest com a
variável de ambiente NODE_OPTIONS (https://jestjs.io/docs/en/ecmascript-
modules):
"scripts": {
"dev": "export DEBUG=livro_nodejs:* &&nodemon server/bin/www",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --coverage
server/**/*.test.js"
},
A parte mais importante dos testes são as asserções; em vez de usar o modulo
assert do core do NodeJS, usaremos o expect do próprio Jest:
import verifyId from './verifyId.js'
import httpErrors from 'http-errors'
let request
let response
let next
beforeEach(() => {
request = {}
response = {}
next = () => {}
})
describe('#verifyId', () => {
it('invalid id', () => {
next = (err) => {
expect(err).toBeDefined()
expect(err).toBeInstanceOf(Error)
expect(err).toBeInstanceOf(httpErrors.HttpError)
expect(err.message).toBe('invalid id')
expect(err.status).toBe(422)
expect(err.stack).toBeDefined()
}
request.params = { id: '5ff' }
verifyId(request, response, next)
})
it('valid id', () => {
next = (err) => {
expect(err).toBe(undefined)
}
request.params = { id: '5ff30c2e7952ec31de6b8e1a' }
verifyId(request, response, next)
})
})
Outra função que podemos extrair, melhorando a reusabilidade e
possibilitando a escrita de um teste unitário isolado, é o handleNotFound, que
estava no server/controller/Stormtrooper.js, e alteramos para import:
import repository from '../repository/Stormtrooper.js'
import { handleNotFound } from './util.js'
const Stormtrooper = {
list(request, response, next) {

Criamos o novo arquivo:
Arquivo server/controller/util.js
import createError from 'http-errors'
const handleNotFound = (result) => {
if (!result) {
throw createError(404, 'trooper not found')
}
return result
}
export { handleNotFound }
E então podemos escrever o teste unitário:
Arquivo server/controller/util.test.js
import { handleNotFound } from './util'
describe('#handleNotFound', () => {
it('when result=null, should throw not found', () => {
const result = null
expect(() => {
handleNotFound(result)
}).toThrowError('trooper not found')
try {
handleNotFound(result)
} catch(err) {
expect(err.status).toBe(404)
}
})
it('when result={}, just return', () => {
const result = {}
const ret = handleNotFound(result)
expect(result).toBe(ret)
})
})
Ao executar o comando npm test:
$ npm test
> [email protected] test /Users/wbruno/Sites/wbruno/livro/capitulo_7/7.3
> NODE_OPTIONS=--experimental-vm-modules jest --coverage server/**/*.test.js
(node:50535) ExperimentalWarning: VM Modules is an experimental feature. This
feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
PASS server/controller/util.test.js
PASS server/middleware/verifyId.test.js
--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
controller | 100 | 100 | 100 | 100 |
util.js | 100 | 100 | 100 | 100 |
middleware | 100 | 100 | 100 | 100 |
verifyId.js | 100 | 100 | 100 | 100 |
--------------|---------|----------|---------|---------|-------------------
Test Suites: 2 passed, 2 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 1.088 s
Ran all test suites matching
/server\/controller\/util.test.js|server\/middleware\/verifyId.test.js/i.
Essas funções são bem simples, e isoladas, o que torna bem fácil de escrever
testes unitários; em outros casos, precisaríamos fazer mocks de dependências,
ou alterar nosso código para trabalhar com injeção de dependências para
facilitar a escrita de testes unitários.
7.4 Testes funcionais
Os testes funcionais testam comunicação com outros serviços, escrita e
leitura no banco de dados, ou seja, todas as dependências e conexões externas
que não testamos nos testes unitários. São testes caixas-pretas, por meio dos
quais fingimos não conhecer o código em si da aplicação, mas apenas a
interface de uso. Logo, teremos testes que farão requisições nas rotas da API
e verificarão se o comportamento esperado foi realizado.
Além do Jest, utilizaremos também o módulo supertest
(https://github.com/visionmedia/supertest) para fazer as requisições HTTP
dentro do teste.
$ npm install --save-dev supertest
$ mkdir -p tests/integration
Alteramos o package.json para incluir esses novos testes:
"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"
},
Começaremos por testes básicos das rotas / e /does-not-exists que devem
retornar Ok e NotFound respectivamente. Retornamos a promise do request
para a função it, pois dessa forma informamos o Jest que a requisição
assíncrona para a API completou.
Arquivo tests/integration/main.test.js
import app from '../../server/app'
import request from 'supertest'
describe('main routes', () => {
it('GET /', () => {
return request(app)
.get('/')
.then(result => {
expect(result.text).toBe('Ola s')
})
})
it('not found', () => {
return request(app)
.get('/does-not-exists')
.then(result => {
expect(result.status).toBe(404)
expect(result.body.err).toBe('Not Found')
})
})
})
Outra opção é utilizar async/await. Reescrevendo, fica da forma a seguir:
import app from '../../server/app'
import request from 'supertest'
describe('main routes', () => {
it('GET /', async () => {
const result = await request(app).get('/')
expect(result.text).toBe('Ola s')
})
it('not found', async () => {
const result = await request(app).get('/does-not-exists')
expect(result.status).toBe(404)
expect(result.body.err).toBe('Not Found')
})
})
Escolha aquela que lhe agradar mais.
Usaremos a seguinte estrutura para os testes do CRUD, tendo pelo menos um
teste para cada endpoint.
Arquivo tests/integration/troopers.test.js
describe('Stormtroopers API', () => {
it('GET /troopers', () => {})
it('GET /troopers/:id', () => {})
it('POST /troopers', () => {})
it('PUT /troopers/:id', () => {})
it('DELETE /troopers/:id', () => {})
})
Para os testes de integração executarem de forma isolada, criamos um
arquivo de configuração, com outra conexão de banco de dados; em vez de
usar o banco livro_nodejs, usamos o test.
Arquivo config/test.json
{
"mongo": {
"uri": "mongodb://localhost:27017/test"
}
}
Utilizamos os hooks beforeEach e afterEach, para criar a massa de dados, e
depois limpar as alterações feitas na execução dos testes, para que um teste
não dependa da alteração realizada pelo anterior.
let id
beforeEach(() => {
return db.stormtroopers.insert({ name: 'Jane Doe' })
.then(result => id = result._id.toString())
})
afterEach(() => db.stormtroopers.remove({}))
No beforeEach faço um insert e atribuo o id retornado a uma variável de
escopo alto, assim temos esse ID disponível nos testes que precisam de um
trooper.
Implementamos um teste para cada endpoint, ficando dessa forma a asserção
da listagem:
it('GET /troopers', async () => {
const result = await request(app).get('/troopers')
expect(result.status).toBe(200)
expect(result.body[0].name).toBe('Jane Doe')
})
Lembrando que, quanto mais asserções (expect), melhor, pois são as
comparações que irão garantir que o comportamento esperado está
acontecendo.
Usamos o id criado no beforeEach no teste que recupera um trooper por id:
it('GET /troopers/:id', async () => {
const result = await request(app).get(`/troopers/${id}`)
expect(result.status).toBe(200)
expect(result.body.name).toBe('Jane Doe')
})
Testamos a criação, enviando uma requisição POST, com o payload:
it('POST /troopers', async () => {
const trooper = {
name: 'John Doe',
patent: 'Soldier'
}
const result = await request(app)
.post('/troopers')
.send(trooper)
expect(result.status).toBe(201)
expect(result.body._id).toBeDefined()
expect(result.body.name).toBe('John Doe')
expect(result.body.patent).toBe('Soldier')
})
No endpoint de atualização, enviamos o ID de um registro que já existe e os
campos que vamos modificar.
it('PUT /troopers/:id', async () => {
const trooper = {
patent: 'Soldier'
}
const result = await request(app)
.put(`/troopers/${id}`)
.send(trooper)
expect(result.status).toBe(200)
})
No teste de delete, verificamos apenas o status code.
it('DELETE /troopers/:id', async() => {
const result = await request(app).delete(`/troopers/${id}`)
expect(result.status).toBe(204)
})

7.5 Testes de carga


Com o pacote loadtest (https://github.com/alexfernandez/loadtest),
conseguimos simular uma carga, ou seja, uma quantidade de acesso
simultâneo, como se vários usuários estivessem consultando nossa API, para
assim medir coisas, como a performance da aplicação, como ela se comporta
em consumo de recursos e concorrência. Instalamos como dependência de
desenvolvimento com a flag -D ou --save-dev:
$ npm i -D loadtest
Podemos variar os valores de concorrência e total de requisições, de acordo
com o quanto queremos estressar a aplicação.
const options = {
method: 'GET',
url: 'http://localhost:3000/troopers',
maxRequests: 1000,
concurrency: 100,
contentType: 'application/json',
headers: {},
insecure: true,
statusCallback
}
Para esse teste de carga, faremos um GET em /troopers.
Arquivo tests/load/get-troopers.js
import loadtest from 'loadtest'
const statusCallback = (error, result, latency) => {
if (error) console.error('error', error)
if (parseInt(result?.requestIndex,10) % 100 === 0) {
console.log('Request index: ', result?.requestIndex)
}
}
const options = {
method: 'GET',
url: 'http://localhost:3000/troopers',
maxRequests: 1000,
concurrency: 100,
contentType: 'application/json',
headers: {},
insecure: true,
statusCallback
}
loadtest.loadTest(options, (error, result) => {
if (error) {
return console.log('Got an error: %s', error)
}
console.log(result)
})
Não coloquei o sufixo .test.js para não rodar junto dos testes de
funcionalidade, então executaremos invocando o arquivo diretamente:
$ node tests/load/get-troopers.js
Request index: 0
Request index: 100
Request index: 200
Request index: 300
Request index: 400
Request index: 500
Request index: 600
Request index: 700
Request index: 800
Request index: 900
{
totalRequests: 1000,
totalErrors: 0,
totalTimeSeconds: 0.720812164,
rps: 1387,
meanLatencyMs: 69.4,
maxLatencyMs: 98,
minLatencyMs: 34,
percentiles: { '50': 66, '90': 89, '95': 90, '99': 93 },
errorCodes: {},
instanceIndex: 0
}
É importante observar os percentis, que trazem informações mais ricas sobre
a experiência de quem usa a API, mais do que simplesmente olhar o tempo
médio de resposta de todos os requests. Nessa execução do teste de carga, a
média foi de 69 milissegundos, enquanto o percentil 95 foi de 90
milissegundos, isso significa que, de todos os requests realizados, 95%
responderam em até 90 ms. Mas o fato de o percentil 90% estar próximo da
média mostra que estamos saudáveis.
8
CAPÍTULO
Produção

Por mais que a ansiedade para colocar no ar seja grande, alguns


procedimentos são muito importantes para ter uma aplicação sólida em
produção. Então, antes de ver como configurar a nossa aplicação NodeJS em
um servidor cloud, vamos dar uma olhada no que podemos fazer para garantir
a monitoria, estabilidade e segurança.

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.2 APM (Aplication Performance Monitoring)


Um APM (Aplication Performance Monitoring), como o New Relic
(https://newrelic.com), ou o AppDynamics (https://www.appdynamics.com),
é um serviço pago que, por meio de agentes instalados no servidor e na
aplicação, consegue monitorar coisas muito importantes, como tempo de
resposta, taxa de erros, consumo de CPU, memória RAM, log etc.
Para integrar o New Relic com o ExpressJS, por exemplo, basta seguir três
passos, ter um arquivo newrelic.js na raiz do projeto, com o nome do projeto e
a key da licença do New Relic:
Arquivo newrelic.js
exports.config = {
app_name: ['{{ new_relic_app_name }}'],
license_key: '{{new_relic_license_key}}',
apdex_t: 4,
logging: {
level: 'error'
},
strip_exception_messages: {
enabled: false
},
error_collector: {
enabled: true,
ignore_status_codes: [301,302,401,403,404,409,410,418,422]
}
}
Nesse arquivo configuramos, por exemplo, quais status code são normais da
aplicação e não devem contabilizar como erros no dashboard.
Temos que instalar o pacote como dependência
(https://github.com/newrelic/node-newrelic):
$ npm i --save newrelic
e fazer require/import do módulo, dentro do código da aplicação, como uma
das primeiras linhas a serem executadas, em nosso caso diretamente no
server/bin/www.js:
require('newrelic')
ou
import newrelic from 'newrelic'
Os três passos descritos já fazem a integração mínima funcionar. Depois
disso, podemos explorar outras diversas opções, como custom attributes,
traces e log.

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.7 MongoDB Atlas


O MongoDB Atlas (https://www.mongodb.com/cloud/atlas) é um SaaS para
servidores de MongoDB. Ele disponibiliza uma instância de MongoDB de
500 MB grátis para testes. Após fazer login, vá em Create a new cluster,
conforme indicado na Figura 8.2.

Figura 8.2 – Interface de criação de um novo cluster MongoDB no


MongoDB Atlas.
Observe a label free tier available para criar esse cluster de graça. Após alguns
minutos, quando o MongoDB Atlas tiver terminado, na aba Clusters, clique
em Connect, mongo shell, e copie a linha de conexão, que se parece com esta
abaixo:
mongo "mongodb+srv://<cluster name>.mongodb.net/<dbname>" --username <usuario>
Na sessão Security, aba Database Access, você adiciona um novo usuário e
uma nova senha (Figura 8.3).

Figura 8.3 – Interface de criação de usuário.


Criado, podemos configurar a aplicação para utilizar o MongoDB Atlas em
vez de usar o banco local.
Arquivo config/default.json
{
"mongo": {
"uri": "mongodb+srv://<usuario>:<senha>@<nome do
cluster>.mongodb.net/<banco_dados>"
}
}
O banco é novo, portanto estará sem nenhum dado. Vamos subir a API com
npm run dev e mandar um POST para criar um soldado:
$ curl -d '{"name": "FN-2187", "nickname": "Finn"}' -H 'content-type: application/json'
http://localhost:3000/troopers
{"name":"FN-2187","nickname":"Finn","_id":"5ff71762860c8d05c4479f3c"}

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).

Figura 8.7 – t2.micro.


Escolhemos a key pair que acabamos de criar e esperamos a EC2 ficar
disponível (Figura 8.8).

Figura 8.8 – Escolha da key pair.


Localmente ainda, iremos fazer compactar o projeto em tar.gz para copiar para
o servidor com scp:
$ tar -czf livro_nodejs.tar.gz 8.1
$ scp -i ~/.ssh/wbruno.pem livro_nodejs.tar.gz [email protected]:/tmp
Feito isso, podemos logar com ssh na máquina, utilizando o IP público:
$ ssh -i ~/.ssh/wbruno.pem root@54…
E aí começamos a configurar o NodeJS, usando o nvm, como é recomendado
pela AWS:
[ec2-user@... ~]$ curl -o- https://raw.githubusercontent.com/nvm-
sh/nvm/v0.37.2/install.sh | bash
[ec2-user@... ~]$ nvm install node
Vamos criar um diretório para enviar a aplicação e extrair o tar.gz dentro
dele:
[ec2-user@... ~]$ mkdir -p /var/www/livro_nodejs
[ec2-user@... ~]$ cd /var/www/livro_nodejs/
[ec2-user@... livro_nodejs]$ tar -xzf /tmp/livro_nodejs.tar.gz
[ec2-user@... livro_nodejs]$ mv 8.1/* .
[ec2-user@... livro_nodejs]$ rm -rf 8.1
Agora, executamos o npm install dentro do servidor e podemos iniciar a
aplicação:
[ec2-user@... livro_nodejs]$ npm i
[ec2-user@... livro_nodejs]$ node server/bin/www.js
Falta liberar no security group, acesso a porta 3000; para isso, crie uma nova
custom TPC inbound rule (Figura 8.9).

Figura 8.9 – Inbound rules.


E pronto, estará disponível para acesso:
$ curl http://ec2-….sa-east-1.compute.amazonaws.com:3000
Ola s
$ curl http://54….:3000
Ola s
Usando tanto o public DNS quanto o IPv4 público, conseguimos agora
acessar a aplicação via HTTP. Toda vez que uma instância EC2 for desligada
(state stop) e depois iniciada (state start), o IP público será modificado, como
podemos verificar na Figura 8.10.
Figura 8.10 – Security group com a EC2 na AWS.

8.8.1 unix service


Vamos configurar a nossa aplicação para ser um serviço do servidor; assim,
caso o servidor seja reiniciado, o unix service se encarregará de instanciar
novamente a aplicação e nos proporcionará uma interface de administração
dentro das boas práticas.
O uso do unix service faz sentido se o deploy for feito em servidores fixos,
como VMs ou VPS; para Kubernetes, outras técnicas são utilizadas, já que é
o contêiner que fica responsável por reiniciar o processo.
Usaremos a instalação do NodeJS feita pelo nvm e precisamos garantir que
os arquivos da aplicação estejam no usuário ec2-user:
[ec2-user@... livro_nodejs]$ which node
~/.nvm/versions/node/v15.5.1/bin/node
[ec2-user@... livro_nodejs]$ chown -R ec2-user:ec2-user /var/www/livro_nodejs
Para visualizar os logs do systemd, execute:
journalctl -u livro_nodejs.service

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

Jan 07 15:48:53 ip-172-31-44-190.sa-east-1.compute.internal systemd[1]: Started livro


nodejs.
Jan 07 15:48:53 ip-172-31-44-190.sa-east-1.compute.internal systemd[1]: Starting livro
nodejs...
Isso quer dizer que está tudo certo e já podemos acessar a nossa API na porta
3000.
Uma configuração bacana do systemd é pedir para que o próprio OS se
encarregue de reiniciar o serviço em caso de falhas, eliminando a necessidade
do forever ou pm2:
Restart=áster
Por conta dessa linha que colocamos no arquivo livro_nodejs.service.
8.8.2 Nginx
Para instalar o Nginx na EC2, rode os seguintes comandos, mais detalhes e
formas de instalar na documentação do Nginx
(https://docs.nginx.com/nginx/admin-guide/installing-nginx/installing-nginx-
open-source/):
[ec2-user@... livro_nodejs]$ sudo yum -y update
[ec2-user@... livro_nodejs]$ sudo áster-linux-extras install epel
[ec2-user@... livro_nodejs]$ sudo yum install nginx
$ nginx -v
nginx version: nginx/1.16.1
Vamos criar um diretório para o Nginx escrever os logs:
[ec2-user@... livro_nodejs]$ sudo mkdir -p /var/log/livro_nodejs/
E criamos o arquivo livro_nodejs.conf no diretório /etc/nginx/conf.d/ do servidor,
frequentemente precisamos do IP do usuário e, para isso, adicionamos as
seguintes linhas na configuração do Nginx:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;

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).

Figura 8.11 – Criar nova aplicação no Heroku.


Crie um arquivo chamado Procfile (sem extensão) na raiz do projeto com o
seguinte conteúdo:
Procfile
web: npm start -p $PORT
O npm start deve estar configurado, sem dependência de desenvolvimento:
"scripts": {
"start": "node server/bin/www.js"
},
Devemos receber a porta via variável de ambiente, ou então usar a 8080:
const server = app.listen(process.env.PORT || 8080, () => log('server started'))
pois o Heroku irá enviar uma porta aleatória:
2021-01-07T19:28:44.297687+00:00 heroku[web.1]: Starting process with command
`npm start -p 11418`
Indicamos no package.json a versão do NodeJS que queremos utilizar:
"engines": {
"node": ">=14.0.0"
},
Configuramos o Heroku como remote:
$ heroku login
$ git init
$ heroku git:remote -a livro-nodejs
Ficaremos então com dois remotes:
$ git remote -v
heroku https://git.heroku.com/livro-nodejs.git (fetch)
heroku https://git.heroku.com/livro-nodejs.git (push)
origin [email protected]:wbruno/livro-nodejs-projeto.git (fetch)
origin [email protected]:wbruno/livro-nodejs-projeto.git (push)
Para fazer deploy, basta enviar um push.
$ git commit --allow-empty -m "deploy: heroku"
$ git push heroku main

remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Node.js app detected
remote:
remote: -----> Creating runtime environment
remote:
remote: NPM_CONFIG_LOGLEVEL=error
remote: NODE_ENV=production
remote: NODE_MODULES_CACHE=true
remote: NODE_VERBOSE=false
remote:
remote: -----> Installing binaries
remote: engines.node (package.json): >=14.0.0
remote: engines.npm (package.json): unspecified (use default)
remote:
remote: Resolving node version >=14.0.0...
remote: Downloading and installing node 15.5.1...
remote: Using default npm version: 7.3.0
remote:
remote: -----> Installing dependencies
remote: Installing node modules (package.json)

remote: https://livro-nodejs.herokuapp.com/ deployed to Heroku
remote: Verifying deploy... done.
To https://git.heroku.com/livro-nodejs.git
* [new branch] main -> main
Após o deploy, o Heroku terá disponibilizado a aplicação na URL:
https://<nome da aplicação>.herokuapp.com. Ou também podemos integrar
o Heroku diretamente ao GitHub, apenas clicando em botões da interface
(Figura 8.12).

Figura 8.12 – Integrando o Heroku ao GitHub.


Assim, todos os pushs na brand padrão irão causar um build e deploy no
Heroku.

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).

Figura 8.13 – Integração do Travis com GitHub.


Liberamos a permissão, conforme a Figura 8.14, e escolhemos a qual
repositório queremos que o travis se integre:
Figura - 8.14 – Escolha de repositório para aprovar permissão.
E, no próximo git push que fizermos, o Travis executará os testes (Figura
8.15).
"scripts": {
"start": "node server/bin/www.js",
"dev": "export DEBUG=livro_nodejs:* &&nodemon server/bin/www",
"test": "jest tests/*.test.js"
},

Figura 8.15 – Testes no Travis.


Vemos que o Travis faz uma marcação nos commits quando o build naquele
commit passa ou falha (Figura 8.16).

Capítulo 8.16 – Resultado do build do Travis nos commits.


Fica legal adicionar um badge ao README.md do projeto usando código
Markdown, do site http://shields.io, para mostrar o build no Travis:
https://img.shields.io/travis/wbruno/livro-nodejs-projeto.

8.11 GitHub Actions


O GitHub (https://github.com) lançou uma poderosa plataforma de integração
chamada GitHub Actions (https://github.com/features/actions). Podemos, por
exemplo, em vez de usar o Travis, executar os testes diretamente no actions,
entre outras coisas, como fazer build de monorepo, publicar pacotes etc.
Arquivo .github/workflows/node.js.yml
name: Unit tests Livro NodeJS CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [15.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build --if-present
- run: npm test
E, na aba actions, podemos ver os testes que foram executados (Figura 8.17).

Figura 8.17 – Inteface do GitHub Actions.


A integração do badge, fica assim:
https://img.shields.io/github/workflow/status/<usuário github>/<nome do
projeto> /<nome por extenso do workflow>.
Referências bibliográficas

CANTELON, M. et al. Node.js in action. Shelter Island, NY: Manning, 2014.


HOWS, D. et al. Introdução ao MongoDB. São Paulo: Novatec, 2015.
JARGAS, A. M. Shell script profissional. São Paulo: Novatec, 2008.
WILSON, M. Construindo aplicações Node com MongoDB e Backbone. São
Paulo: Novatec, 2013.
Referências eletrônicas
JSON Web Tokens. Disponível em: https://jwt.io.
MDN Web Docs, Javascript. Disponível em: https://developer.mozilla.org/pt-
BR/docs/Web/JavaScript.
NodeJS. Disponível em: https://nodejs.org/api/.
Origami Jedi Master Yoda. Disponível em: https://www.youtube.com/watch?
v=U5B71d1OR_M.
REST. Disponível em:
https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm.
Understanding ECMAScript 6. Disponível em:
https://github.com/nzakas/understandinges6.
Aprendendo a desenvolver aplicações
web
Purewal, Semmy
9788575227381
360 páginas

Compre agora e leia

Domine os fundamentos do desenvolvimento de aplicações web


implementando uma aplicação simples a partir do zero, baseada em
banco de dados, usando HTML, JavaScript e outras ferramentas de
código aberto. Por meio de tutoriais que permitem pôr a mão na
massa, este guia prático mostra como criar uma interface de usuário,
implementar um servidor, desenvolver uma comunicação cliente-
servidor e usar um serviço baseado em nuvem para implantar a
aplicação aos desenvolvedores inexperientes de aplicações web.
Todo capítulo inclui problemas práticos, exemplos completos e
modelos mentais do fluxo de trabalho do desenvolvimento. Este livro,
ideal para disciplinas de nível universitário, ajuda você a dar início ao
desenvolvimento de aplicações web, proporcionando uma base sólida
durante o processo. •Defina um fluxo de trabalho básico com um
editor de texto, um sistema de controle de versões e um navegador
web •Estruture uma interface de usuário com HTML e inclua estilos
usando CSS •Use jQuery e JavaScript para acrescentar interatividade
à sua aplicação •Faça a ligação entre o cliente e o servidor por meio
de AJAX, objetos JavaScript e JSON •Aprenda o básico da
programação do lado do servidor com o Node.js •Armazene dados
fora de sua aplicação usando Redis e MongoDB •Compartilhe sua
aplicação carregando-a na nuvem com o CloudFoundry •Obtenha
dicas básicas sobre como escrever códigos que facilitem a
manutenção, tanto no cliente quanto no servidor.

Compre agora e leia


Candlestick
Debastiani, Carlos Alberto
9788575225943
200 páginas

Compre agora e leia

A análise dos gráficos de Candlestick é uma técnica amplamente


utilizada pelos operadores de bolsas de valores no mundo inteiro. De
origem japonesa, este refinado método avalia o comportamento do
mercado, sendo muito eficaz na previsão de mudanças em
tendências, o que permite desvendar fatores psicológicos por trás dos
gráficos, incrementando a lucratividade dos investimentos.
Candlestick – Um método para ampliar lucros na Bolsa de Valores é
uma obra bem estruturada e totalmente ilustrada. A preocupação do
autor em utilizar uma linguagem clara e acessível a torna leve e de
fácil assimilação, mesmo para leigos. Cada padrão de análise
abordado possui um modelo com sua figura clássica, facilitando a
identificação. Depois das características, das peculiaridades e dos
fatores psicológicos do padrão, é apresentado o gráfico de um caso
real aplicado a uma ação negociada na Bovespa. Este livro possui,
ainda, um índice resumido dos padrões para pesquisa rápida na
utilização cotidiana.

Compre agora e leia


Manual de Análise Técnica
Abe, Marcos
9788575227022
256 páginas

Compre agora e leia

Este livro aborda o tema Investimento em Ações de maneira inédita e


tem o objetivo de ensinar os investidores a lucrarem nas mais
diversas condições do mercado, inclusive em tempos de crise.
Ensinará ao leitor que, para ganhar dinheiro, não importa se o
mercado está em alta ou em baixa, mas sim saber como operar em
cada situação. Com o Manual de Análise Técnica o leitor aprenderá: -
os conceitos clássicos da Análise Técnica de forma diferenciada, de
maneira que assimile não só os princípios, mas que desenvolva o
raciocínio necessário para utilizar os gráficos como meio de
interpretar os movimentos da massa de investidores do mercado; -
identificar oportunidades para lucrar na bolsa de valores, a longo e
curto prazo, até mesmo em mercados baixistas; um sistema de
investimentos completo com estratégias para abrir, conduzir e fechar
operações, de forma que seja possível maximizar lucros e minimizar
prejuízos; - estruturar e proteger operações por meio do
gerenciamento de capital. Destina-se a iniciantes na bolsa de valores
e investidores que ainda não desenvolveram uma metodologia própria
para operar lucrativamente.
Compre agora e leia
Avaliando Empresas, Investindo em
Ações
Debastiani, Carlos Alberto
9788575225974
224 páginas

Compre agora e leia

Avaliando Empresas, Investindo em Ações é um livro destinado a


investidores que desejam conhecer, em detalhes, os métodos de
análise que integram a linha de trabalho da escola fundamentalista,
trazendo ao leitor, em linguagem clara e acessível, o conhecimento
profundo dos elementos necessários a uma análise criteriosa da
saúde financeira das empresas, envolvendo indicadores de balanço e
de mercado, análise de liquidez e dos riscos pertinentes a fatores
setoriais e conjunturas econômicas nacional e internacional. Por meio
de exemplos práticos e ilustrações, os autores exercitam os conceitos
teóricos abordados, desde os fundamentos básicos da economia até
a formulação de estratégias para investimentos de longo prazo.

Compre agora e leia


Microsserviços prontos para a produção
Fowler, Susan J.
9788575227473
224 páginas

Compre agora e leia

Um dos maiores desafios para as empresas que adotaram a


arquitetura de microsserviços é a falta de padronização de arquitetura
– operacional e organizacional. Depois de dividir uma aplicação
monolítica ou construir um ecossistema de microsserviços a partir do
zero, muitos engenheiros se perguntam o que vem a seguir. Neste
livro prático, a autora Susan Fowler apresenta com profundidade um
conjunto de padrões de microsserviço, aproveitando sua experiência
de padronização de mais de mil microsserviços do Uber. Você
aprenderá a projetar microsserviços que são estáveis, confiáveis,
escaláveis, tolerantes a falhas, de alto desempenho, monitorados,
documentados e preparados para qualquer catástrofe. Explore os
padrões de disponibilidade de produção, incluindo: Estabilidade e
confiabilidade – desenvolva, implante, introduza e descontinue
microsserviços; proteja-se contra falhas de dependência.
Escalabilidade e desempenho – conheça os componentes essenciais
para alcançar mais eficiência do microsserviço. Tolerância a falhas e
prontidão para catástrofes – garanta a disponibilidade forçando
ativamente os microsserviços a falhar em tempo real. Monitoramento
– aprenda como monitorar, gravar logs e exibir as principais métricas;
estabeleça procedimentos de alerta e de prontidão. Documentação e
compreensão – atenue os efeitos negativos das contrapartidas que
acompanham a adoção dos microsserviços, incluindo a dispersão
organizacional e a defasagem técnica.

Compre agora e leia

Você também pode gostar