a
Node.js e Express
Vamos focar no back-end nesta parte: ou seja, na implementação de funcionalidades no lado do servidor.
Estaremos construindo nosso back-end utilizando NodeJS, que é um ambiente de execução JavaScript baseado no motor JavaScript Chrome V8 do Google.
O conteúdo desta parte do curso foi escrita com base na versão v18.13.0 do Node.js. Certifique-se de que a versão do seu Node é pelo menos tão nova quanto a versão utilizada aqui (você pode verificar a versão executando node -v na linha de comando).
Como mencionado na Parte 1, os navegadores ainda não suportam as novas funcionalidades de JavaScript, e é por isso que o código em execução no navegador deve ser transpilado com o babel, por exemplo. Mas a situação é diferente com JavaScript em execução no back-end. A versão mais recente do Node suporta a maioria das últimas funcionalidades de JavaScript, então podemos usar as últimas funcionalidades sem ter que transpilar nosso código.
Nosso objetivo é implementar um back-end que funcione com a aplicação de notas da Parte 2. No entanto, vamos começar com o básico implementando um clássico programa "Olá, mundo!".
Observe que nem todas as aplicações e exercícios nesta parte são aplicações React, e não usaremos o utilitário create-react-app para inicializar o projeto para esta aplicação.
Já tínhamos mencionado o npm na Parte 2, que é uma ferramenta usada para gerenciar pacotes JavaScript. Na verdade, o npm é originário do ecossistema Node.
Vamos navegar até um diretório apropriado e criar um novo modelo para nossa aplicação com o comando npm init. Vamos responder as perguntas apresentadas pelo utilitário, e o resultado será um arquivo package.json gerado automaticamente na raiz do projeto que contém as informações do projeto.
{
"name": "backend",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Matti Luukkainen",
"license": "MIT"
}
O arquivo define, por exemplo, que o ponto de entrada da aplicação é o arquivo index.js.
Vamos fazer uma pequena alteração no objeto scripts:
{
// ...
"scripts": {
"start": "node index.js", "test": "echo \"Error: no test specified\" && exit 1"
},
// ...
}
Agora, vamos criar a primeira versão da nossa aplicação adicionando um arquivo index.js à raiz do projeto com o seguinte código:
console.log('hello world')
Podemos executar o programa diretamente com o Node a partir da linha de comando:
node index.js
Ou podemos executá-lo como um script npm:
npm start
O script npm start funciona porque o definimos no arquivo package.json:
{
// ...
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
// ...
}
Embora a execução do projeto funcione quando ele é iniciado chamando node index.js a partir da linha de comando, é costume de projetos npm executar tarefas como scripts npm.
Por padrão, o arquivo package.json também define outro script npm comumente usado chamado npm test. Como nosso projeto ainda não possui uma biblioteca de testes, o comando npm test apenas executa o seguinte comando:
echo "Error: no test specified" && exit 1
Um servidor web simples
Vamos transformar a aplicação em um servidor web editando o arquivo index.js da seguinte maneira:
const http = require('http')
const app = http.createServer((request, response) => {
response.writeHead(200, { 'Content-Type': 'text/plain' })
response.end('Hello World')
})
const PORT = 3001
app.listen(PORT)
console.log(`Server running on port ${PORT}`)
Uma vez que a aplicação está em execução, a seguinte mensagem é impressa no console:
Server running on port 3001
Podemos abrir nossa humilde aplicação no navegador entrando no endereço http://localhost:3001:
O servidor funciona da mesma maneira independentemente da última parte da URL, por isso o endereço http://localhost:3001/foo/bar exibirá o mesmo conteúdo.
Obs.: se a porta 3001 já estiver sendo usada por alguma outra aplicação, iniciar o servidor resultará na seguinte mensagem de erro:
➜ hello npm start
> hello@1.0.0 start /Users/mluukkai/opetus/_2019fullstack-code/part3/hello
> node index.js
Server running on port 3001
events.js:167
throw er; // Unhandled 'error' event
^
Error: listen EADDRINUSE :::3001
at Server.setupListenHandle [as _listen2] (net.js:1330:14)
at listenInCluster (net.js:1378:12)
Você tem duas opções: ou encerre a aplicação usando a porta 3001 (o json-server na última parte do material estava usando a porta 3001), ou use uma porta diferente para esta aplicação.
Vamos olhar mais de perto na primeira linha do código:
const http = require('http')
Na primeira linha, a aplicação importa o módulo integrado web server do Node. Isso é praticamente o que já estávamos fazendo em nosso código no lado do navegador, mas com uma sintaxe um pouco diferente:
import http from 'http'
Hoje em dia, o código que roda no navegador usa módulos ES6. Os módulos são definidos com um export e usados com um import.
No entanto, Node.js usa módulos chamados CommonJS. A razão para isso é que o ecossistema Node teve a necessidade de usar módulos muito antes de JavaScript os suportar na especificação da linguagem. Node agora também suporta o uso de módulos ES6, mas como o suporte ainda não é totalmente perfeito, vamos aderir aos módulos CommonJS.
Os módulos CommonJS funcionam quase exatamente como os módulos ES6, pelo menos no que diz respeito às nossas necessidades neste curso.
O próximo trecho em nosso código é assim:
const app = http.createServer((request, response) => {
response.writeHead(200, { 'Content-Type': 'text/plain' })
response.end('Hello World')
})
O código usa o método createServer ("criarServidor") do módulo http para criar um novo servidor web. Um gerenciador de evento é registrado no servidor que é chamado sempre que uma requisição HTTP é feita para o endereço http://localhost:3001 do servidor.
A requisição é respondida com o código de status 200, com o cabeçalho Content-Type definido como text/plain, e o conteúdo do site a ser retornado definido como Hello World.
As últimas linhas vinculam o servidor http atribuído à variável app para ouvir as requisições HTTP enviadas à porta 3001:
const PORT = 3001
app.listen(PORT)
console.log(`Server running on port ${PORT}`)
O objetivo principal do servidor back-end neste curso é oferecer dados brutos em formato JSON para o front-end. Por esse motivo, vamos imediatamente alterar nosso servidor para retornar uma lista codificada de notas no formato JSON:
const http = require('http')
let notes = [ { id: 1, content: "HTML is easy", important: true }, { id: 2, content: "Browser can execute only JavaScript", important: false }, { id: 3, content: "GET and POST are the most important methods of HTTP protocol", important: true }]const app = http.createServer((request, response) => { response.writeHead(200, { 'Content-Type': 'application/json' }) response.end(JSON.stringify(notes))})
const PORT = 3001
app.listen(PORT)
console.log(`Server running on port ${PORT}`)
Vamos reiniciar o servidor (você pode encerrá-lo pressionando ctrl + c no console) e atualizar o navegador.
O valor application/json no cabeçalho Content-Type informa o receptor de que os dados estão no formato JSON. O array notes é transformado em JSON com o método JSON.stringify(notes).
Quando abrimos o navegador, o formato de exibição das notas é o mesmo que vimos na Parte 2 quando usamos o json-server para servir a lista de notas:
Express
É possível implementar nosso código do servidor diretamente com o servidor web http integrado do Node. No entanto, isso é cansativo, especialmente quando a aplicação fica maior.
Muitas bibliotecas foram desenvolvidas para facilitar o desenvolvimento do lado do servidor com Node, oferecendo uma interface mais agradável para trabalhar com o módulo integrado http. Essas bibliotecas visam fornecer uma melhor abstração para casos de uso geral que normalmente exigimos para construir um servidor back-end. De longe, a biblioteca mais popular destinada a esse fim é o Express.
Vamos usar o Express definindo-o como uma dependência do projeto com o comando:
npm install express
A dependência também é adicionada ao nosso arquivo package.json:
{
// ...
"dependencies": {
"express": "^4.18.2"
}
}
O código-fonte da dependência é instalado no diretório node_modules localizado na raiz do projeto. Além do Express, você pode encontrar uma grande quantidade de outras dependências no diretório:
Essas são as dependências da biblioteca Express e as dependências de todas as suas dependências e assim por diante. Elas são chamadas de dependências transitivas (transitive dependencies) do nosso projeto.
A versão 4.18.2 da biblioteca Express foi instalada em nosso projeto. O que significa esse acento circunflexo na frente do número de versão em package.json?
"express": "^4.18.2"
O modelo de versionamento usado no npm é chamado de versionamento semântico (semantic versioning).
O acento circunflexo na frente de ^4.18.2 significa que se e quando as dependências de um projeto forem atualizadas, a versão instalada do Express será pelo menos 4.18.2. No entanto, a versão instalada do Express também pode ter um número de patch maior (o último número) ou um número minor maior (o número do meio). A versão principal da biblioteca indicada pelo primeiro número major deve ser a mesma.
Podemos atualizar as dependências do projeto com o comando:
npm update
Igualmente, se começarmos a trabalhar no projeto em outro computador, podemos instalar todas as dependências atualizadas do projeto definidas em package.json executando comando a seguir no diretório raiz do projeto:
npm install
Se o número major de uma dependência não mudar, então as novas versões devem ser compatíveis com versões anteriores. Isso significa que se nossa aplicação vier a usar a versão 4.99.175 do Express no futuro, então todo o código implementado nesta parte ainda terá que funcionar sem a necessidade de alterações no código. Em contraste, a futura versão 5.0.0 do Express pode conter alterações que farão com que nossa aplicação não funcione mais.
Web e Express
Vamos voltar à nossa aplicação e fazer as seguintes alterações:
const express = require('express')
const app = express()
let notes = [
...
]
app.get('/', (request, response) => {
response.send('<h1>Hello World!</h1>')
})
app.get('/api/notes', (request, response) => {
response.json(notes)
})
const PORT = 3001
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})
Para colocar a nova versão de nossa aplicação em uso, temos que reiniciar a aplicação.
A aplicação não mudou muito. Logo no início do nosso código, importamos o express que desta vez é uma função usada para criar uma aplicação express armazenada na variável app:
const express = require('express')
const app = express()
Em seguida, definimos duas rotas para a aplicação. A primeira define um gerenciador de evento que é usado para lidar com requisições HTTP GET feitas na raiz / da aplicação:
app.get('/', (request, response) => {
response.send('<h1>Hello World!</h1>')
})
A função de gerência de evento aceita dois parâmetros. O primeiro parâmetro request (requisição) contém todas as informações da requisição HTTP, e o segundo parâmetro response (resposta) é usado para definir como a requisição é respondida.
Em nosso código, a requisição é respondida usando o método send (enviar) do objeto response. Ao chamar o método, o servidor responde à requisição HTTP enviando uma resposta contendo a string <h1>Hello World!</h1>
que foi passada para o método send. Como o parâmetro é uma string, o express define automaticamente o valor do cabeçalho Content-Type como text/html. O código de status da resposta é definido como 200 por padrão.
Podemos verificar isso na guia Rede nas Ferramentas do Desenvolvedor:
A segunda rota define um gerenciador de evento que lida com requisições HTTP GET feitas no caminho notes da aplicação:
app.get('/api/notes', (request, response) => {
response.json(notes)
})
A requisição é respondida com o método json do objeto response. Quando chamado, o método enviará o array notes que foi passado como uma string formatada em JSON. O express define automaticamente o cabeçalho Content-Type com o valor apropriado de application/json.
Em seguida, vamos dar uma olhada rápida nos dados enviados no formato JSON.
Na versão anterior do código em que estávamos usando apenas Node, tivemos que transformar os dados no formato JSON com o método JSON.stringify:
response.end(JSON.stringify(notes))
Com Express isso se torna desnecessário, porque essa transformação acontece automaticamente.
Vale ressaltar que JSON (JavaScript Object Notation [Notação de Objeto JavaScript]) é uma string e não um objeto JavaScript como o valor que foi atribuído a notes.
O experimento mostrado abaixo ilustra esse ponto:
O experimento acima foi feito no node-repl interativo. Você pode iniciar o node-repl interativo digitando node no terminal. O repl é particularmente útil para testar como os comandos funcionam enquanto você está escrevendo o código da aplicação. Eu mais que recomendo o uso dessa ferramenta!
nodemon
Se fizermos alterações no código da aplicação, precisamos reiniciá-la para ver as alterações. Reiniciamos a aplicação primeiro encerrando-a pressionando ctrl + c e depois reiniciando-a. Se compararmos isso ao conveniente fluxo de trabalho em React, em que o navegador é recarregado automaticamente após as alterações serem feitas, parece até um pouco trabalhoso.
A solução para esse problema é o nodemon:
nodemon irá monitorar os arquivos no diretório em que ele foi iniciado, e se houver alguma alteração nos arquivos, o nodemon reiniciará automaticamente sua aplicação Node.
Vamos instalar o nodemon definindo-o como uma dependência de desenvolvimento (development dependency) com o comando:
npm install --save-dev nodemon
O conteúdo do arquivo package.json também foi alterado:
{
//...
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^2.0.20"
}
}
Se você usou acidentalmente o comando errado e a dependência nodemon foi adicionada em "dependencies" em vez de "devDependencies", altere manualmente o conteúdo de package.json para que corresponda ao que é mostrado acima.
Por dependências de desenvolvimento, estamos nos referindo a ferramentas que são necessárias apenas durante o desenvolvimento da aplicação, por exemplo, para testar ou reiniciar automaticamente a aplicação, como o nodemon.
Essas dependências de desenvolvimento não são necessárias quando a aplicação é executada na fase de produção em um servidor de produção (Fly.io ou Heroku, por exemplo).
Podemos iniciar nossa aplicação com o nodemon assim:
node_modules/.bin/nodemon index.js
Alterações no código da aplicação agora fazem com que o servidor seja reiniciado automaticamente. Vale ressaltar que, embora o servidor back-end seja reiniciado automaticamente, o navegador ainda deve ser atualizado manualmente. Isso ocorre porque, ao contrário do que acontece ao trabalhar em React, não temos a funcionalidade hot reload (grosso modo, "recarga rápida") necessária para recarregar automaticamente o navegador.
O comando é longo e bastante desagradável, portanto, vamos definir um script npm dedicado para ele no arquivo package.json:
{
// ..
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js", "test": "echo \"Error: no test specified\" && exit 1"
},
// ..
}
Não é necessário especificar no script o caminho node_modules/.bin/nodemon para o nodemon, pois o npm pesquisa automaticamente pelo arquivo nesse diretório.
Agora podemos iniciar o servidor no modo de desenvolvimento com o comando:
npm run dev
Ao contrário dos scripts start e test, também temos que adicionar run ao comando.
REST
Vamos expandir nossa aplicação para que ela forneça a mesma API HTTP RESTful do json-server.
Representational State Transfer, também conhecido como REST, foi introduzido em 2000 na dissertação de PhD de Roy Fielding. REST é um estilo arquitetural destinado a construir aplicações web escaláveis.
Não vamos aprofundar a definição de REST de Fielding ou gastar tempo ponderando sobre o que é ou não RESTful. Em vez disso, adotamos uma visão mais restrita, preocupando-nos apenas com a típica compreensão de APIs RESTful em aplicações web. A definição original de REST não se limita somente à aplicações web.
Mencionamos na parte anterior que coisas singulares, como as notas no caso da nossa aplicação, são chamadas de recursos no modo RESTful de pensar. Cada recurso tem uma URL associada que é o endereço exclusivo do recurso.
Uma convenção para criar endereços exclusivos é combinar o nome do tipo de recurso com o identificador exclusivo do recurso.
Vamos assumir que a URL raiz do nosso serviço é www.example.com/api.
Se definirmos o tipo de recurso da nota como notes, então o endereço de um recurso de "note" com o identificador 10 tem o endereço exclusivo www.example.com/api/notes/10.
A URL para toda a coleção de todos os recursos de notes é www.example.com/api/notes.
Podemos executar diferentes operações em recursos. A operação a ser executada é definida pelo verbo HTTP:
URL | verbo | funcionalidade |
---|---|---|
notes/10 | GET | busca um único recurso |
notes | GET | busca todos os recursos na coleção |
notes | POST | cria um novo recurso baseado nos dados requisitados |
notes/10 | DELETE | exclui um recurso identificado |
notes/10 | PUT | substitui todo o recurso identificado com os dados requisitados |
notes/10 | PATCH | substitui uma parte do recurso identificado com os dados requisitados |
É assim que conseguimos definir aproximadamente o que REST chama de interface uniforme (uniform interface), que significa uma maneira consistente de definir interfaces que tornam possível a cooperação entre sistemas.
Essa forma de interpretação do modelo REST se enquadra no segundo nível de maturidade RESTful (second level of RESTful maturity) no Modelo de Maturidade de Richardson. De acordo com a definição fornecida por Roy Fielding, ainda não definimos o que é uma API REST. Na verdade, a grande maioria das APIs "REST" do mundo não atende aos critérios originais de Fielding delineados em sua dissertação.
Em alguns lugares (ver, por exemplo, Richardson, Ruby: RESTful Web Services), verá nosso modelo para uma API CRUD simples será referenciado como um exemplo de arquitetura orientada a recursos (resource-oriented architecture) em vez de REST. Vamos evitar ficar presos discutindo semântica e, em vez disso, voltar a trabalhar em nossa aplicação.
Buscando um único recurso
Vamos expandir nossa aplicação para que ela ofereça uma interface REST para operar em notas individuais. Primeiro, vamos criar uma rota para buscar um único recurso.
O endereço único que usaremos para uma nota individual é na forma notes/10, onde o número no final refere-se ao número de identificação único da nota.
Podemos definir parâmetros para rotas no Express usando a sintaxe de dois-pontos:
app.get('/api/notes/:id', (request, response) => {
const id = request.params.id
const note = notes.find(note => note.id === id)
response.json(note)
})
Agora, app.get('/api/notes/:id', ...)
gerenciará todas as requisições HTTP GET que estão na forma /api/notes/X, onde X é uma string arbitrária.
O parâmetro id na rota de uma requisição pode ser acessado por meio do objeto request:
const id = request.params.id
O agora familiar método de arrays find é usado para encontrar a nota com um ID que corresponde ao parâmetro. A nota é então retornada ao remetente da requisição.
Quando testamos nossa aplicação acessando http://localhost:3001/api/notes/1 em nosso navegador, percebemos que ela não parece funcionar, pois o navegador exibe uma página vazia. Isso não é surpresa para nós, desenvolvedores de software, pois é hora de depurar.
Adicionar comandos console.log em nosso código já é um truque comprovado pelo tempo:
app.get('/api/notes/:id', (request, response) => {
const id = request.params.id
console.log(id)
const note = notes.find(note => note.id === id)
console.log(note)
response.json(note)
})
Quando visitamos novamente o endereço http://localhost:3001/api/notes/1 no navegador, o console — que é o terminal (neste caso) — exibirá o seguinte:
O parâmetro de id da rota é passado para nossa aplicação, mas o método find não encontra uma nota correspondente.
Para aprofundar nossa investigação, também adicionamos um console.log dentro da função de comparação passada para o método find. Para fazer isso, temos que nos livrar da sintaxe de função de seta compactada note => note.id === id, e usar a sintaxe com uma declaração explícita de retorno:
app.get('/api/notes/:id', (request, response) => {
const id = request.params.id
const note = notes.find(note => {
console.log(note.id, typeof note.id, id, typeof id, note.id === id)
return note.id === id
})
console.log(note)
response.json(note)
})
Quando visitamos a URL novamente no navegador, cada chamada à função de comparação imprime algumas coisas diferentes no console. A saída do console é a seguinte:
1 'number' '1' 'string' false 2 'number' '1' 'string' false 3 'number' '1' 'string' false
A causa do bug fica clara. A variável id contém uma string '1', enquanto os ids das notas são números inteiros. Em JavaScript, o comparador de igualdade estrita === considera que todos os valores de tipos diferentes não são iguais por padrão, o que significa que 1 não é igual a '1'.
Vamos corrigir o problema mudando o parâmetro id de uma string para um Number (construtor Number):
app.get('/api/notes/:id', (request, response) => {
const id = Number(request.params.id) const note = notes.find(note => note.id === id)
response.json(note)
})
Agora, a busca de um recurso individual funciona.
No entanto, há outro problema com nossa aplicação.
Se procurarmos uma nota com um id que não existe, o servidor responde com:
O código de status HTTP retornado é 200, o que significa que a resposta teve sucesso. Não são retornados dados com a resposta, uma vez que o valor do cabeçalho content-length é 0, e o mesmo pode ser verificado no navegador.
A razão para esse comportamento é que a variável note é definida como "undefined" se nenhuma nota correspondente for encontrada. A situação precisa ser gerenciada no servidor de forma correta. Se nenhuma nota for encontrada, o servidor deve responder com o código de status 404 not found ("404 não encontrado(a)") em vez de 200.
Vamos fazer a seguinte alteração em nosso código:
app.get('/api/notes/:id', (request, response) => {
const id = Number(request.params.id)
const note = notes.find(note => note.id === id)
if (note) { response.json(note) } else { response.status(404).end() }})
Como nenhum dado está anexado à resposta, usamos o método status para definir o status e o método end para responder à requisição sem enviar nenhum dado.
A condição if aproveita o fato de que todos os objetos JavaScript são truthy (verdade/verdadeiro), o que significa que eles avaliam como verdadeiros em uma operação de comparação. No entanto, undefined é falsy (falso/falsidade), o que significa que ele avaliará como falso.
Nossa aplicação funciona e envia o código de status de erro se nenhuma nota for encontrada. No entanto, a aplicação não informa nada ao usuário — como as aplicações web normalmente fazem — quando visitamos uma página que não existe. Não precisamos exibir nada no navegador porque as APIs REST são interfaces destinadas ao uso programático, e o código de status de erro já é o necessário para o caso.
De qualquer forma, é possível dar uma pista sobre a razão para enviar um erro 404 substituindo a mensagem padrão NOT FOUND.
Excluindo recursos
A seguir, vamos implementar uma rota para excluir recursos. A exclusão ocorre fazendo uma requisição HTTP DELETE para a URL do recurso:
app.delete('/api/notes/:id', (request, response) => {
const id = Number(request.params.id)
notes = notes.filter(note => note.id !== id)
response.status(204).end()
})
Se a exclusão do recurso for bem-sucedida, ou seja, se a nota existir e for removida, respondemos à requisição com o código de status 204 no content ("200 nenhum conteúdo") e não retornamos nenhum dado com a resposta.
Não há consenso sobre qual código de status deve ser retornado para uma requisição DELETE se o recurso não existir. As únicas duas opções são 204 e 404. Para simplificar, nossa aplicação responderá com 204 em ambos os casos.
Postman
Então, como testar a operação de exclusão? As requisições HTTP GET são fáceis de fazer a partir do navegador. Poderíamos escrever algum JavaScript para testar a exclusão, mas escrever código de teste nem sempre é a melhor solução em todas as situações.
Existem muitas ferramentas para tornar mais fácil a realização de testes em back-ends. Uma delas é o programa de linha de comando curl. No entanto, em vez do curl, vamos dar uma olhada em como usar o Postman para testar a aplicação.
Vamos baixar deste site o cliente desktop do Postman e testá-lo:
É bastante fácil usar o Postman nesta situação. É suficiente definir a URL e selecionar o tipo de requisição correta (DELETE).
O servidor back-end parece responder corretamente. Ao fazer uma requisição HTTP GET para http://localhost:3001/api/notes, vemos que a nota com o id 2 não está mais na lista, o que indica que a exclusão foi bem-sucedida.
Como as notas na aplicação são salvas apenas na memória, a lista de notas retornará ao seu estado original quando reiniciarmos a aplicação.
O cliente REST do Visual Studio Code
Se quiser o usar o Visual Studio Code, é possível utilizar o plugin VS Code REST client em vez do Postman.
É muito fácil usar o plugin depois de instalado. Criamos um diretório na raiz da aplicação chamado requests. Salvamos todas as requisições do cliente REST no diretório como arquivos que terminam com a extensão .rest.
Vamos criar um novo arquivo get_all_notes.rest e definir a requisição que busca todas as notas.
Ao clicar no texto Send Request, o cliente REST executará a requisição HTTP e a resposta do servidor será aberta no editor.
O cliente HTTP do WebStorm
Se você usar o IntelliJ WebStorm, é possível fazer um procedimento semelhante com o Cliente HTTP integrado. Crie um novo arquivo com extensão .rest
e o editor exibirá suas opções para criar e executar suas requisições. Saiba mais sobre o processo seguindo este guia.
Recebendo dados
Em seguida, vamos implementar a funcionalidade de adicionar novas notas ao servidor. É possível adicionar uma nota fazendo uma requisição HTTP POST para o endereço http://localhost:3001/api/notes e enviando todas as informações para a nova nota no corpo (body) da requisição em formato JSON.
Para que possamos acessar os dados facilmente, precisamos da ajuda do json-parser do Express, que é usado com o comando app.use(express.json()).
Vamos ativar o json-parser e implementar um gerenciador inicial para lidar com requisições HTTP POST:
const express = require('express')
const app = express()
app.use(express.json())
//...
app.post('/api/notes', (request, response) => { const note = request.body console.log(note) response.json(note)})
A função do gerenciador de evento pode acessar os dados da propriedade body do objeto request.
Sem o json-parser, a propriedade body seria indefinida. O json-parser funciona de forma que ele pega os dados JSON de uma requisição, transforma-os em um objeto JavaScript e, em seguida, anexa-os à propriedade body do objeto request antes do gerenciador de rota ser chamado.
Por enquanto, a aplicação não faz nada com os dados recebidos, exceto imprimi-los no console e enviá-los de volta na resposta.
Antes de implementarmos o restante da lógica da aplicação, vamos verificar no Postman se os dados são recebidos pelo servidor. Além de definir a URL e o tipo de requisição no Postman, também temos que definir os dados enviados no body:
A aplicação imprime no console os dados que enviamos na requisição:
Obs.: Mantenha o terminal visível o tempo todo enquanto a aplicação estiver sendo executada quando estiver trabalhando no back-end. Graças ao Nodemon, quaisquer alterações que fizermos no código reiniciarão a aplicação. Se você prestar atenção no console, poderá identificar imediatamente os erros que ocorrem na aplicação:
Da mesma forma, é útil verificar o console para garantir que o back-end está se comportando da forma que esperamos em diferentes situações, como quando enviamos dados com uma requisição HTTP POST. Naturalmente, é uma boa ideia adicionar muitos comandos console.log ao código enquanto a aplicação ainda estiver sendo desenvolvida.
Uma possível causa de problemas é um cabeçalho Content-Type definido incorretamente em requisições. Isso pode acontecer com o Postman se o tipo body não estiver definido corretamente:
O cabeçalho Content-Type é definido como text/plain:
O servidor parece receber apenas um objeto vazio:
O servidor não será capaz de analisar corretamente os dados sem o valor correto no cabeçalho. Ele nem tentará adivinhar o formato dos dados, já que há uma quantidade enorme de potenciais Content-Types.
Se você estiver usando o VS Code, deverá instalar o cliente REST do capítulo anterior agora, se ainda não tiver instalado. A requisição POST pode ser enviada com o cliente REST assim:
Criamos um novo arquivo create_note.rest para a requisição. A requisição é formatada de acordo com as instruções da documentação.
Uma vantagem que o cliente REST tem sobre o Postman é que as requisições estão disponíveis convenientemente na raiz do repositório do projeto e podem ser distribuídas para todos na equipe de desenvolvimento. Você também pode adicionar várias requisições no mesmo arquivo usando separadores ###
:
GET http://localhost:3001/api/notes/
###
POST http://localhost:3001/api/notes/ HTTP/1.1
content-type: application/json
{
"name": "sample",
"time": "Wed, 21 Oct 2015 18:27:50 GMT"
}
O Postman também permite que os usuários salvem requisições, mas a situação pode ficar bastante caótica, especialmente quando você está trabalhando em vários projetos não relacionados.
Observação importante
Às vezes, ao depurar, é possível que você queira descobrir quais cabeçalhos foram definidos na requisição HTTP. Uma maneira de fazer isso é através do método get do objeto request, que pode ser usado para obter o valor de um único cabeçalho. O objeto request também possui a propriedade headers, que contém todos os cabeçalhos de uma requisição específica.
Podem ocorrer problemas com o cliente REST do VS Code se você adicionar acidentalmente uma linha vazia entre a linha superior e a linha que especifica os cabeçalhos HTTP. Nessa situação, o cliente REST interpreta como se todos os cabeçalhos estivessem vazios, o que faz com que o servidor back-end não saiba que os dados que recebeu estão no formato JSON.
Você será capaz de identificar esse cabeçalho faltando Content-Type se em algum momento no seu código você imprimir todos os cabeçalhos da requisição com o comando console.log(request.headers).
Vamos voltar para a aplicação. Depois de verificar se a aplicação recebe dados corretamente, é hora de finalizar o gerenciamento da requisição:
app.post('/api/notes', (request, response) => {
const maxId = notes.length > 0
? Math.max(...notes.map(n => n.id))
: 0
const note = request.body
note.id = maxId + 1
notes = notes.concat(note)
response.json(note)
})
Precisamos de um id único para a nota. Primeiro, descobrimos o maior número de id na lista atual e o atribuímos à variável maxId. O id da nova nota é então definido como maxId + 1. Este método não é recomendado, mas vamos conviver com ele por enquanto, pois o substituiremos em breve.
A versão atual ainda tem o problema de que a requisição HTTP POST pode ser usada para adicionar objetos com propriedades arbitrárias. Vamos melhorar a aplicação definindo que a propriedade content não pode estar vazia. A propriedade important receberá o valor padrão false. Todas as outras propriedades são descartadas:
const generateId = () => {
const maxId = notes.length > 0
? Math.max(...notes.map(n => n.id))
: 0
return maxId + 1
}
app.post('/api/notes', (request, response) => {
const body = request.body
if (!body.content) {
return response.status(400).json({
error: 'content missing'
})
}
const note = {
content: body.content,
important: body.important || false,
id: generateId(),
}
notes = notes.concat(note)
response.json(note)
})
A lógica para gerar às notas um novo número de ID foi extraída para uma função separada generateId.
Se os dados recebidos estiverem faltando um valor para a propriedade content, o servidor responderá à requisição com o código de status 400 bad request ("400 requisição inválida"):
if (!body.content) {
return response.status(400).json({
error: 'content missing'
})
}
Observe que declarar o return é crucial porque, caso contrário, o código será executado até o final e a nota mal formatada será salva na aplicação.
Se a propriedade content tiver um valor, a nota será baseada nos dados recebidos. Se estiver faltando a propriedade important, definimos o valor padrão como false. O valor padrão é gerado atualmente de uma forma bastante estranha:
important: body.important || false,
Se os dados salvos na variável body tiverem a propriedade important, a expressão resultará no seu valor. Se a propriedade não existir, a expressão resultará em false, que é definido no lado direito das barras verticais.
Sendo mais preciso, quando a propriedade important é false, então a expressão body.important || false retornará de fato o false do lado direito...
É possível encontrar o código atual completo da nossa aplicação na branch part3-1 neste repositório do GitHub.
O código para o estado atual da aplicação é especificado na branch part3-1.
Se você clonar o projeto, execute o comando npm install antes de iniciar a aplicação com npm start ou npm run dev.
Mais uma coisa antes de prosseguirmos para os exercícios. A função para gerar IDs é esta:
const generateId = () => {
const maxId = notes.length > 0
? Math.max(...notes.map(n => n.id))
: 0
return maxId + 1
}
O corpo da função contém uma linha que parece um tanto intrigante:
Math.max(...notes.map(n => n.id))
O que exatamente está acontecendo nessa linha de código? notes.map(n => n.id) cria um novo array que contém todos os IDs das notas. Math.max retorna o valor máximo dos números que lhe são passados. No entanto, notes.map(n => n.id) é um array, então ele não pode ser dado diretamente como parâmetro para Math.max. O array pode ser transformado em números individuais usando a sintaxe de espalhamento ou sintaxe de "três pontos" spread ...
Sobre os tipos de requisição HTTP
O padrão HTTP fala sobre duas propriedades relacionadas aos tipos de requisição, segurança (safety) e idempotência (idempotency).
A requisição HTTP GET deve ser segura:
Em particular, a convenção estabelecida diz que os métodos GET e HEAD NÃO devem ter a importância de realizar uma ação além da recuperação. Esses métodos devem ser considerados "seguros".
Segurança significa que a execução da requisição não deve causar quaisquer efeitos colaterais no servidor. Por efeitos colaterais, entendemos que o estado do banco de dados não deve mudar como resultado da requisição, e a resposta deve retornar apenas os dados que já existem no servidor.
Nada garante que uma requisição GET é segura, esta é apenas uma recomendação definida no padrão HTTP. Adotando os princípios RESTful em nossa API, as requisições GET são sempre usadas de forma que são seguras.
O padrão HTTP também define que o tipo de requisição HEAD deve ser seguro. Na prática, o HEAD deve funcionar exatamente como o GET, mas não retorna nada além do código de status e dos cabeçalhos de resposta. O corpo da resposta não será retornado quando você fizer uma requisição HEAD.
Todas as requisições HTTP, exceto o POST, devem ser idempotentes:
Os métodos também podem ter a propriedade de "idempotência", no sentido de que (além de problemas de erro ou expiração) os efeitos colaterais de N > 0 requisições idênticas são os mesmos que para uma única requisição. Os métodos GET, HEAD, PUT e DELETE compartilham essa propriedade.
Isso significa que se uma requisição não gera efeitos colaterais, o resultado deve ser o mesmo, independentemente de quantas vezes a requisição for enviada.
Se fizermos uma requisição HTTP PUT para a URL /api/notes/10 e com a requisição enviarmos os dados { content: "sem efeitos colaterais!", important: true }, o resultado será o mesmo, independentemente de quantas vezes a requisição for enviada.
Assim como segurança para a requisição GET, idempotência também é apenas uma recomendação no padrão HTTP e não algo que possa ser garantido simplesmente com base no tipo de requisição. No entanto, quando nossa API adere aos princípios RESTful, as requisições GET, HEAD, PUT e DELETE são usadas de forma que são idempotentes.
POST é o único tipo de requisição HTTP que não é nem seguro nem idempotente. Se enviarmos 5 requisições HTTP POST diferentes para /api/notes com um corpo de { content: "muitas iguais", important: true }, as 5 notas resultantes no servidor terão o mesmo conteúdo.
Middleware
O json-parser do Express que usamos anteriormente é um middleware.
Middleware são funções que podem ser usadas para lidar com objetos de request e response.
O json-parser que usamos anteriormente pega os dados brutos das requisições armazenadas no objeto request, os decompõe em um objeto JavaScript e os atribui ao objeto request como uma nova propriedade body.
Na prática, é possível usar vários middlewares ao mesmo tempo. Quando você tem mais de um, eles são executados um por um na ordem em que foram adicionados no Express.
Vamos implementar nosso próprio middleware que imprime informações sobre cada requisição enviada ao servidor.
Middleware é uma função que recebe três parâmetros:
const requestLogger = (request, response, next) => {
console.log('Method:', request.method)
console.log('Path: ', request.path)
console.log('Body: ', request.body)
console.log('---')
next()
}
No final do corpo da função, a função next que foi passada como parâmetro é chamada. A função next cede o controle para o próximo middleware.
Middleware é usado assim:
app.use(requestLogger)
Funções de middleware são chamadas na ordem em que são adicionadas ao objeto do servidor Express com o método use. Observe que o json-parser é adicionado antes do middleware requestLogger, caso contrário, request.body não será inicializado quando o registrador (logger) for executado!
Funções de middleware devem ser adicionadas antes das rotas se quisermos que sejam executadas antes que os gerenciadores de evento da rota sejam chamados. Também há situações em que queremos definir funções de middleware depois das rotas. Na prática, isso significa que estamos definindo funções de middleware que só são chamadas se nenhuma rota gerenciar a requisição HTTP.
Vamos adicionar o middleware a seguir depois das nossas rotas. Este middleware será usado para pegar requisições feitas para rotas inexistentes. Para essas requisições, o middleware retornará uma mensagem de erro no formato JSON.
const unknownEndpoint = (request, response) => {
response.status(404).send({ error: 'unknown endpoint' })
}
app.use(unknownEndpoint)
É possível encontrar o código da nossa aplicação atual na íntegra na branch part3-2 neste repositório do GitHub.