035.2 Lição 1
Certificação: |
Web Development Essentials |
---|---|
Versão: |
1.0 |
Tópico: |
035 Programação do servidor NodeJS |
Objetivo: |
035.3 Noções básicas de NodeJS Express |
Lição: |
1 de 2 |
Introdução
O Express.js, ou simplesmente Express, é um framework popular que roda em Node.js e é usado para escrever servidores HTTP que lidam com solicitações de clientes de aplicativos web. O Express oferece suporte a diversas maneiras de ler parâmetros enviados por HTTP.
Script inicial do servidor
Para demonstrar os recursos básicos do Express ao receber e tratar solicitações, vamos simular um aplicativo que solicita algumas informações ao servidor. Em particular, o servidor de exemplo:
-
Fornece uma função
echo
, que simplesmente retorna a mensagem enviada pelo cliente. -
Informa ao cliente seu endereço IP mediante solicitação.
-
Usa cookies para identificar clientes conhecidos.
O primeiro passo é criar o arquivo JavaScript que funcionará como servidor. Usando npm
, crie um diretório chamado myserver
com o arquivo JavaScript:
$ mkdir myserver $ cd myserver/ $ npm init
No ponto de entrada, qualquer nome de arquivo pode ser usado. Aqui, usaremos o nome de arquivo padrão: index.js
. A lista a seguir mostra um arquivo index.js
básico que será usado como ponto de entrada para o nosso servidor:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.get('/', (req, res) => {
res.send('Request received')
})
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
Algumas constantes importantes para a configuração do servidor são definidas nas primeiras linhas do script. As duas primeiras, express
e app
, correspondem ao módulo express
incluído e a uma instância desse módulo que executa nosso aplicativo. Adicionaremos as ações a serem realizadas pelo servidor ao objeto app
.
As outras duas constantes, host
e port
, definem o host e a porta de comunicação associada ao servidor.
Caso você tenha um host acessível publicamente, use o nome dele ao invés de myserver
como o valor de host
. Se o nome do host não for informado, o Express usará por padrão localhost
, o computador em que o aplicativo é executado. Nesse caso, nenhum cliente externo poderá acessar o programa, o que pode ser bom para testes, mas oferece pouco valor em um contexto de produção.
É imprescindível informar a porta, ou o servidor não poderá ser iniciado.
Este script anexa apenas dois procedimentos ao objeto app
: a ação app.get()
, que responde às solicitações feitas por clientes através de HTTP GET
, e a chamada app.listen()
, necessária para ativar o servidor e que atribui a ele um host e uma porta.
Para iniciar o servidor, basta executar o comando node
, fornecendo o nome do script como argumento:
$ node index.js
Assim que a mensagem Server ready at http://myserver:8080
aparecer, o servidor estará pronto para receber as solicitações de um cliente HTTP. As solicitações podem ser feitas a partir de um navegador no mesmo computador em que o servidor está sendo executado ou a partir de outra máquina que possa acessar o servidor.
Todos os detalhes da transação que veremos aqui são mostrados no navegador, bastando abrir a janela do console do desenvolvedor. Alternativamente, o comando curl
pode ser usado para comunicação HTTP, permitindo inspecionar mais facilmente os detalhes da conexão. Caso não esteja familiarizado com a linha de comando do shell, você pode criar um formulário HTML para enviar solicitações a um servidor.
O exemplo a seguir mostra como usar o comando curl
na linha de comando para fazer uma solicitação HTTP ao servidor recém-implantado:
$ curl http://myserver:8080 -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > GET / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/html; charset=utf-8 < Content-Length: 16 < ETag: W/"10-1WVvDtVyAF0vX9evlsFlfiJTT5c" < Date: Fri, 02 Jul 2021 14:35:11 GMT < Connection: keep-alive < * Connection #0 to host myserver left intact Request received
A opção -v
do comando curl
exibe todos os cabeçalhos de solicitação e resposta, bem como outras informações de depuração. As linhas que começam com >
indicam os cabeçalhos de solicitação enviados pelo cliente e as linhas que começam com <
indicam os cabeçalhos de resposta enviados pelo servidor. As linhas começando com *
são informações geradas pelo próprio curl
. O conteúdo da resposta é mostrado apenas no final, que neste caso é a linha Request received
.
A URL do serviço, que aqui contém o nome do host do servidor e a porta (http://myserver:8080
), foram dados como argumentos para o comando curl
. Como nenhum diretório ou nome de arquivo é fornecido, o padrão é o diretório raiz /
. A barra aparece como o arquivo de solicitação na linha > GET / HTTP/1.1
, que é seguido, na saída, pelo nome do host e porta.
Além de exibir cabeçalhos de conexão HTTP, o comando curl
auxilia no desenvolvimento de aplicativos, permitindo enviar dados para o servidor usando diferentes métodos HTTP e em diferentes formatos. Essa flexibilidade torna mais fácil depurar quaisquer problemas e implementar novos recursos no servidor.
Rotas
As solicitações que o cliente pode fazer ao servidor dependem de quais rotas foram definidas no arquivo index.js
. Uma rota especifica um método HTTP e define um caminho (mais precisamente, um URI) que pode ser solicitado pelo cliente.
Até aqui, o servidor tem apenas uma rota configurada:
app.get('/', (req, res) => {
res.send('Request received')
})
Embora seja uma rota muito simples, que retorna apenas uma mensagem de texto puro ao cliente, ela basta para identificar os componentes mais importantes usados para estruturar a maioria das rotas:
-
O método HTTP servido pela rota. No exemplo, o método HTTP
GET
é indicado pela propriedadeget
do objetoapp
. -
O caminho servido pela rota. Quando o cliente não especifica um caminho para a solicitação, o servidor usa o diretório raiz, que é o diretório base reservado para uso do servidor web. Um exemplo posterior neste capítulo usa o caminho
/echo
, que corresponde a uma solicitação feita amyserver:8080/echo
. -
A função executada quando o servidor recebe uma solicitação nesta rota, geralmente escrita de forma abreviada como uma função de seta porque a sintaxe
=>
aponta para a definição da função sem nome. O parâmetroreq
(abreviação de “request”) e o parâmetrores
(abreviação de “response”) fornecem detalhes sobre a conexão, passada para a função pela própria instância do aplicativo.
Método POST
Para estender a funcionalidade de nosso servidor de teste, vamos ver como definir uma rota para o método HTTP POST
. Ele é usado pelos clientes que precisam enviar ao servidor dados extras, além dos que estão incluídos no cabeçalho da solicitação. A opção --data
do comando curl
invoca automaticamente o método POST
e inclui o conteúdo que será enviado ao servidor via POST
. A linha POST / HTTP/1.1
na saída a seguir mostra que o método POST
foi empregado. No entanto, nosso servidor definiu apenas um método GET
, por isso ocorre um erro quando usamos curl
para enviar uma solicitação via POST
:
$ curl http://myserver:8080/echo --data message="This is the POST request body" * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > POST / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > Content-Length: 37 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 37 out of 37 bytes * Mark bundle as not supporting multiuse < HTTP/1.1 404 Not Found < X-Powered-By: Express < Content-Security-Policy: default-src 'none' < X-Content-Type-Options: nosniff < Content-Type: text/html; charset=utf-8 < Content-Length: 140 < Date: Sat, 03 Jul 2021 02:22:45 GMT < Connection: keep-alive < <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Cannot POST /</pre> </body> </html> * Connection #0 to host myserver left intact
No exemplo anterior, executar curl
com o parâmetro --data message="This is the POST request body"
equivale a enviar um formulário contendo um campo de texto chamado message
preenchido com This is the POST request body
.
Como o servidor foi configurado com apenas uma rota para o caminho /
, e essa rota responde apenas ao método HTTP GET
, o cabeçalho de resposta inclui a linha HTTP/1.1 404 Not Found
. Além disso, o Express gerou automaticamente uma curta resposta HTML com o aviso Cannot POST
.
Agora que vimos como gerar uma solicitação POST
usando o curl
, vamos escrever um programa Express capaz de tratar essa solicitação com sucesso.
Primeiro, note que o campo Content-Type
no cabeçalho da solicitação diz que os dados enviados pelo cliente estão no formato application/x-www-form-urlencoded
. O Express não reconhece esse formato por padrão, por isso precisamos usar o módulo express.urlencoded
. Quando incluímos esse módulo, o objeto req
— passado como parâmetro para a função de manipulação (handler) — tem configurada a propriedade req.body.message
, que corresponde ao campo mensagem
enviado pelo cliente. O módulo é carregado com app.use
, que deve ser posto antes da declaração das rotas:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.use(express.urlencoded({ extended: true }))
Feito isso, bastaria alterar app.get
para app.post
na rota existente para atender às solicitações feitas via POST
e para recuperar o corpo da solicitação:
app.post('/', (req, res) => {
res.send(req.body.message)
})
Em vez de substituir a rota, outra possibilidade seria simplesmente adicionar essa nova rota, já que o Express identifica o método HTTP no cabeçalho da solicitação e usa a rota apropriada. Como estamos interessados em adicionar mais de uma funcionalidade a este servidor, é conveniente separar cada uma com seu próprio caminho, como /echo
e /ip
.
Manipulador de caminhos e funções
Tendo definido qual método HTTP responderá à solicitação, precisamos definir um caminho específico para o recurso e uma função que processe e gere uma resposta ao cliente.
Para expandir a funcionalidade echo
do servidor, podemos definir uma rota usando o método POST
com o caminho /echo
:
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
O parâmetro req
da função de manipulação (handler) contém todos os detalhes da solicitação armazenados como propriedades. O conteúdo do campo message
no corpo da solicitação está disponível na propriedade req.body.message
. O exemplo simplesmente envia este campo de volta ao cliente através da chamada res.send(req.body.message)
.
Lembre-se de que as alterações feitas só entram em vigor depois que o servidor é reiniciado. Como estamos executando o servidor a partir de uma janela de terminal para os exemplos dados neste capítulo, você pode desligar o servidor pressionando kbd:[Ctrl+C] nesse terminal. Em seguida, execute novamente o servidor por meio do comando node index.js
. A resposta obtida pelo cliente para a solicitação curl
mostrada anteriormente agora é bem-sucedida:
$ curl http://myserver:8080/echo --data message="This is the POST request body" This is the POST request body
Outras maneiras de passar e devolver informações em uma solicitação GET
Pode ser excessivo usar o método HTTP POST
para enviar apenas mensagens de texto curtas, como a usada no exemplo. Nesses casos, os dados podem ser enviados em uma string de solicitação iniciada por um ponto de interrogação. Assim, a string ?message=This+is+the+message
poderia ser incluída no caminho de solicitação do método HTTP GET
. Os campos usados na string de solicitação estão disponíveis para o servidor na propriedade req.query
. Portanto, um campo denominado message
está disponível na propriedade req.query.message
.
Outra maneira de enviar dados através do método HTTP GET
é usar os parâmetros de rota do Express:
app.get('/echo/:message', (req, res) => {
res.send(req.params.message)
})
A rota neste exemplo corresponde às solicitações feitas com o método GET
usando o caminho /echo/:message
, onde :message
é um espaço reservado para qualquer termo enviado com esse rótulo pelo cliente. Esses parâmetros estão acessíveis na propriedade req.params
. Com esta nova rota, a função echo
do servidor pode ser solicitada de forma mais sucinta pelo cliente:
$ curl http://myserver:8080/echo/hello hello
Em outras situações, as informações de que o servidor precisa para processar a solicitação não precisam ser fornecidas explicitamente pelo cliente. Por exemplo, o servidor tem outra maneira de recuperar o endereço IP público do cliente. Essa informação está presente no objeto req
por padrão, na propriedade req.ip
:
app.get('/ip', (req, res) => {
res.send(req.ip)
})
Agora o cliente pode solicitar o caminho /ip
com o método GET
para encontrar seu próprio endereço IP público:
$ curl http://myserver:8080/ip 187.34.178.12
Outras propriedades do objeto req
podem ser modificadas pelo cliente, especialmente os cabeçalhos de solicitação disponíveis em req.headers
. A propriedade req.headers.user-agent
, por exemplo, identifica qual programa está fazendo a solicitação. Embora não seja uma prática comum, o cliente pode alterar o conteúdo deste campo; assim, o servidor não deve usá-lo para identificar de forma confiável um cliente específico. É ainda mais importante validar os dados fornecidos explicitamente pelo cliente, evitando assim inconsistências nos limites e formatos que podem afetar adversamente o aplicativo.
Ajustes à resposta
Como vimos nos exemplos anteriores, o parâmetro res
é responsável por retornar uma resposta ao cliente. Além disso, o objeto res
pode alterar outros aspectos da resposta. Você deve ter notado que, embora as respostas que implementamos até agora sejam apenas breves mensagens de texto puro, o cabeçalho Content-Type
das respostas está usando text/html; charset=utf-8
. Embora isso não impeça que a resposta em texto puro seja aceita, será mais correto redefinir este campo no cabeçalho da resposta como text/plain
com a configuração res.type('text/plain')
.
Outros tipos de ajustes de resposta envolvem o uso de cookies, que permitem ao servidor identificar um cliente que já fez uma solicitação anteriormente. Os cookies são importantes para a utilização de recursos avançados, como a criação de sessões privadas que associam solicitações a um usuário específico, mas aqui veremos apenas um exemplo simples de como usar um cookie para identificar um cliente que já acessou o servidor.
Dado o design modularizado do Express, o gerenciamento de cookies deve ser instalado com o comando npm
antes de ser usado no script:
$ npm install cookie-parser
Após a instalação, o gerenciamento de cookies precisa ser incluído no script do servidor. A seguinte definição deve ser adicionada perto do início do arquivo:
const cookieParser = require('cookie-parser')
app.use(cookieParser())
Para ilustrar o uso dos cookies, vamos modificar a função de manipulação da rota com o caminho raiz /
já existente no script. O objeto req
possui uma propriedade req.cookies
, em que os cookies enviados no cabeçalho da solicitação são preservados. O objeto res
, por sua vez, possui um método res.cookie()
que cria um novo cookie a ser enviado ao cliente. A função de manipulação no exemplo a seguir verifica se um cookie com o nome known
existe na solicitação. Se tal cookie não existir, o servidor pressupõe que se trata de um visitante que chegou ao site pela primeira vez e envia a ele um cookie com esse nome através da chamada res.cookie('known', '1')
. Atribuímos arbitrariamente o valor 1
ao cookie porque ele precisa ter algum conteúdo, mas o servidor não consulta esse valor. Este aplicativo pressupõe que a simples presença do cookie indica que o cliente já solicitou esta rota antes:
app.get('/', (req, res) => {
res.type('text/plain')
if ( req.cookies.known === undefined ){
res.cookie('known', '1')
res.send('Welcome, new visitor!')
}
else
res.send('Welcome back, visitor');
})
Por padrão, o curl
não usa cookies nas transações. Mas ele tem opções para armazenar (-c cookies.txt
) e enviar cookies armazenados (-b cookies.txt
):
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > GET / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/plain; charset=utf-8 * Added cookie known="1" for domain myserver, path /, expire 0 < Set-Cookie: known=1; Path=/ < Content-Length: 21 < ETag: W/"15-l7qrxcqicl4xv6EfA5fZFWCFrgY" < Date: Sat, 03 Jul 2021 23:45:03 GMT < Connection: keep-alive < * Connection #0 to host myserver left intact Welcome, new visitor!
Como esse comando foi o primeiro acesso desde que os cookies foram implementados no servidor, o cliente não tinha cookies para incluir na solicitação. Como seria de se esperar, o servidor não identificou o cookie na solicitação e, portanto, incluiu o cookie nos cabeçalhos de resposta, conforme indicado na linha Set-Cookie: known=1; Path=/
da saída. Como habilitamos os cookies em curl
, uma nova solicitação incluirá o cookie known=1
nos cabeçalhos da solicitação, permitindo que o servidor identifique a presença do cookie:
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > GET / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > Cookie: known=1 > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/plain; charset=utf-8 < Content-Length: 21 < ETag: W/"15-ATq2flQYtLMYIUpJwwpb5SjV9Ww" < Date: Sat, 03 Jul 2021 23:45:47 GMT < Connection: keep-alive < * Connection #0 to host myserver left intact Welcome back, visitor
Segurança dos cookies
O desenvolvedor deve estar ciente das potenciais vulnerabilidades ao usar cookies para identificar os clientes que fazem solicitações. Os invasores podem usar técnicas como cross-site scripting (XSS) e cross-site request forgery (CSRF) para roubar os cookies de um cliente e, assim, personificá-lo ao fazer uma solicitação ao servidor. De modo geral, esses tipos de ataques usam campos de comentários não validados ou URLs construídas meticulosamente para inserir código JavaScript malicioso na página. Quando executado por um cliente autêntico, esse código pode copiar cookies válidos e armazená-los, ou encaminhá-los para outro destino.
Portanto, especialmente em aplicativos profissionais, é importante instalar e empregar recursos mais especializados do Express, conhecidos como middleware. Os módulos express-session
ou cookie-session
permitem um controle mais completo e seguro sobre a sessão e o gerenciamento de cookies. Esses componentes oferecem controles extras para evitar que os cookies sejam desviados de seu emissor original.
Exercícios Guiados
-
Como o conteúdo do campo
comment
, enviado dentro de uma string de consulta do método HTTPGET
, pode ser lido em uma função de manipulação? -
Escreva uma rota que use o método HTTP
GET
e o caminho/agent
para enviar de volta ao cliente o conteúdo do cabeçalhouser-agent
. -
O Express.js tem um recurso chamado parâmetros de rota, onde um caminho como
/user/:name
pode ser usado para receber o parâmetroname
enviado pelo cliente. Como o parâmetroname
pode ser acessado de dentro da função de manipulação da rota?
Exercícios Exploratórios
-
Se o nome de host de um servidor é
myserver
, qual rota do Express receberia o envio no formulário abaixo?<form action="/contact/feedback" method="post"> ... </form>
-
Durante o desenvolvimento do servidor, o programador não consegue ler a propriedade
req.body
, mesmo depois de verificar se o cliente está enviando o conteúdo corretamente através do método HTTPPOST
. Qual é a causa provável desse problema? -
O que acontece quando o servidor tem uma rota definida para o caminho
/user/:name
e o cliente faz uma solicitação para/user/
?
Resumo
Esta lição explica como escrever scripts do Express para receber e lidar com solicitações HTTP. O Express usa o conceito de rotas para definir os recursos disponíveis aos clientes, o que lhe dá grande flexibilidade para construir servidores para qualquer tipo de aplicativo web. Esta lição aborda os seguintes conceitos e procedimentos:
-
Rotas que usam os métodos HTTP
GET
e HTTPPOST
. -
Como os dados de formulários são armazenados no objeto
request
. -
Como usar parâmetros de rota.
-
Personalização de cabeçalhos de resposta.
-
Gerenciamento básico de cookies.
Respostas aos Exercícios Guiados
-
Como o conteúdo do campo
comment
, enviado dentro de uma string de consulta do método HTTPGET
, pode ser lido em uma função de manipulação?O campo
comment
está disponível na propriedadereq.query.comment
. -
Escreva uma rota que use o método HTTP
GET
e o caminho/agent
para enviar de volta ao cliente o conteúdo do cabeçalhouser-agent
.app.get('/agent', (req, res) => { res.send(req.headers.user-agent) })
-
O Express.js tem um recurso chamado parâmetros de rota, onde um caminho como
/user/:name
pode ser usado para receber o parâmetroname
enviado pelo cliente. Como o parâmetroname
pode ser acessado de dentro da função de manipulação da rota?O parâmetro
name
está acessível na propriedadereq.params.name
.
Respostas aos Exercícios Exploratórios
-
Se o nome de host de um servidor é
myserver
, qual rota do Express receberia o envio no formulário abaixo?<form action="/contact/feedback" method="post"> ... </form>
app.post('/contact/feedback', (req, res) => { ... })
-
Durante o desenvolvimento do servidor, o programador não consegue ler a propriedade
req.body
, mesmo depois de verificar se o cliente está enviando o conteúdo corretamente através do método HTTPPOST
. Qual é a causa provável desse problema?O programador não incluiu o módulo
express.urlencoded
, que permite ao Express extrair o corpo de uma solicitação. -
O que acontece quando o servidor tem uma rota definida para o caminho
/user/:name
e o cliente faz uma solicitação para/user/
?O servidor emite uma resposta
404 Not Found
, porque a rota requer que o parâmetro:name
seja fornecido pelo cliente.