035.2 Lección 1
Certificación: |
Conceptos básicos de desarrollo web |
---|---|
Versión: |
1.0 |
Tema: |
035 Programación NodeJS server |
Objetivo: |
035.2 Conceptos básicos de NodeJS Express |
Lección: |
1 de 2 |
Introducción
Express.js, o simplemente Express, es un marco popular que se ejecuta en Node.js y se usa para escribir servidores HTTP que manejan solicitudes de clientes de aplicaciones web. Express admite muchas formas de leer los parámetros enviados a través de HTTP.
Script de servidor inicial
Para demostrar las funciones básicas de Express para recibir y manejar solicitudes, simulemos una aplicación que solicita información del servidor. En particular, el servidor de ejemplo:
-
Proporciona una función
echo
, que simplemente devuelve el mensaje enviado por el cliente. -
Le dice al cliente su dirección IP a pedido.
-
Utiliza cookies para identificar clientes conocidos.
El primer paso es crear el archivo JavaScript que funcionará como servidor. Usando npm
, cree un directorio llamado myserver
con el archivo JavaScript:
$ mkdir myserver $ cd myserver/ $ npm init
Para el punto de entrada, se puede utilizar cualquier nombre de archivo. Aquí usaremos el nombre de archivo predeterminado: index.js
. La siguiente lista muestra un archivo básico index.js
que se utilizará como punto de entrada para nuestro 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}`)
})
Algunas constantes importantes para la configuración del servidor se definen en las primeras líneas del script. Los dos primeros, express
y app
, corresponden al módulo express
incluido y una instancia de este módulo que ejecuta nuestra aplicación. Agregaremos las acciones a realizar por el servidor al objeto app
.
Las otras dos constantes, host
y port
, definen el host y el puerto de comunicación asociados al servidor.
Si tiene un host de acceso público, use su nombre en lugar de myserver
como valor de host
. Si no proporciona el nombre de host, Express se establecerá de forma predeterminada en localhost
, la computadora donde se ejecuta la aplicación. En ese caso, ningún cliente externo podrá comunicarse con el programa, lo que puede estar bien para realizar pruebas pero ofrece poco valor en la producción.
Es necesario proporcionar el puerto o el servidor no se iniciará.
Este script adjunta solo dos procedimientos al objeto app
: the app.get()
que responde a las solicitudes realizadas por los clientes a través de HTTP GET
, y la llamada app.listen()
, que se requiere para activar el servidor y le asigna un host y un puerto.
Para iniciar el servidor, simplemente ejecute el comando node
, proporcionando el nombre del script como argumento:
$ node index.js
Tan pronto como aparezca el mensaje Server ready at http://myserver:8080
, el servidor estará listo para recibir solicitudes de un cliente HTTP. Las solicitudes se pueden realizar desde un navegador en la misma computadora donde se ejecuta el servidor, o desde otra máquina que pueda acceder al servidor.
Todos los detalles de la transacción que veremos aquí se muestran en el navegador si abre una ventana para la consola del desarrollador. Alternativamente, el comando curl
se puede usar para la comunicación HTTP y le permite inspeccionar los detalles de la conexión más fácilmente. Si no está familiarizado con la línea de comandos de shell, puede crear un formulario HTML para enviar solicitudes a un servidor.
El siguiente ejemplo muestra cómo usar el comando curl
en la línea de comandos para realizar una solicitud HTTP al servidor recién implementado:
$ 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
La opción -v
del comando curl
muestra todos los encabezados de solicitud y respuesta, así como otra información de depuración. Las líneas que comienzan con >
indican los encabezados de solicitud enviados por el cliente y las líneas que comienzan con <
indican los encabezados de respuesta enviados por el servidor. Las líneas que comienzan con *
son información generada por el propio curl
. El contenido de la respuesta se muestra solo al final, que en este caso es la línea Request received
.
La URL del servicio, que en este caso contiene el nombre de host y el puerto del servidor (http://myserver:8080
), se proporcionaron como argumentos para el comando curl
. Debido a que no se proporciona ningún directorio o nombre de archivo, estos valores predeterminados son el directorio raíz /
. La barra diagonal aparece como el archivo de solicitud en la línea > GET / HTTP/1.1
, seguida en la salida por el nombre de host y el puerto.
Además de mostrar los encabezados de conexión HTTP, el comando curl
ayuda al desarrollo de la aplicación permitiéndole enviar datos al servidor usando diferentes métodos HTTP y en diferentes formatos. Esta flexibilidad hace que sea más fácil depurar cualquier problema e implementar nuevas funciones en el servidor.
Rutas
Las solicitudes que el cliente puede hacer al servidor dependen de las rutas que se hayan definido en el archivo index.js
. Una ruta especifica un método HTTP y define un path (más precisamente, un URI) que puede ser solicitado por el cliente.
Hasta ahora, el servidor solo tiene una ruta configurada:
app.get('/', (req, res) => {
res.send('Request received')
})
Aunque es una ruta muy simple, simplemente devolver un mensaje de texto sin formato al cliente, es suficiente para identificar los componentes más importantes que se utilizan para estructurar la mayoría de las rutas:
-
El método HTTP servido por la ruta. En el ejemplo, el método HTTP
GET
se indica mediante la propiedadget
del objetoapp
. -
El camino servido por la ruta. Cuando el cliente no especifica una ruta para la solicitud, el servidor usa el directorio raíz, que es el directorio base reservado para que lo use el servidor web. Un ejemplo posterior en este capítulo usa la ruta
/echo
, que corresponde a una solicitud hecha amyserver:8080/echo
. -
La función ejecutada cuando el servidor recibe una solicitud en esta ruta, generalmente escrita en forma abreviada como una flecha porque la sintaxis
=>
apunta a la definición de la función sin nombre. El parámetroreq
(abreviatura de “request”) y el parámetrores
(abreviatura de “response”) brindan detalles sobre la conexión, pasados a la función por la propia instancia de la aplicación.
Método POST
Para ampliar la funcionalidad de nuestro servidor de prueba, veamos cómo definir una ruta para el método HTTP POST
. Los clientes lo utilizan cuando necesitan enviar datos adicionales al servidor además de los incluidos en el encabezado de la solicitud. La opción --data
del comando curl
invoca automáticamente el método POST
e incluye contenido que se enviará al servidor a través de POST
. La línea POST / HTTP/1.1
en el siguiente resultado muestra que se utilizó el método POST
. Sin embargo, nuestro servidor definió solo un método GET
, por lo que ocurre un error cuando usamos curl
para enviar una solicitud a través de 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
En el ejemplo anterior, ejecutar curl
con el parámetro --data message="This is the POST request body"
es equivalente a enviar un formulario que contiene el campo de texto llamado message
, rellenado con This is the POST request body
.
Debido a que el servidor está configurado con una sola ruta /
, y esa ruta solo responde al método HTTP GET
, el encabezado de respuesta tiene la línea HTTP/1.1 404 Not Found
. Además, Express generó automáticamente una breve respuesta HTML con la advertencia Cannot POST
.
Habiendo visto cómo generar una solicitud POST
a través de curl
, vamos a escribir un programa Express que pueda manejar la solicitud con éxito.
Primero, tenga en cuenta que el campo Content-Type
en el encabezado de la solicitud que los datos enviados por el cliente están en el formato application/x-www-form-urlencoded
. Express no reconoce ese formato por defecto, por lo que necesitamos usar el módulo express.urlencoded
. Cuando incluimos este módulo, el objeto req
enviado como parámetro a la función del controlador, tiene la propiedad req.body.message
establecida, que corresponde al campo message
enviado por el cliente. El módulo se carga con app.use
, que debe colocarse antes de la declaración de rutas:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.use(express.urlencoded({ extended: true }))
Una vez hecho esto, sería suficiente cambiar app.get
a app.post
en la ruta existente para cumplir con las solicitudes realizadas a través de POST
y recuperar el cuerpo de la solicitud:
app.post('/', (req, res) => {
res.send(req.body.message)
})
En lugar de reemplazar la ruta, otra posibilidad sería simplemente agregar esta nueva ruta, porque Express identifica el método HTTP en el encabezado de la solicitud y usa la ruta apropiada. Debido a que estamos interesados en agregar más de una funcionalidad a este servidor, es conveniente separar cada una con su propia ruta, como /echo
y /ip
.
Controlador de ruta y función
Habiendo definido qué método HTTP responderá a la solicitud, ahora necesitamos definir una ruta específica para el recurso y una función que procesa y genera una respuesta al cliente.
Para expandir la funcionalidad echo
del servidor, podemos definir una ruta usando el método POST
con la ruta /echo
:
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
El parámetro req
de la función del controlador contiene todos los detalles de la solicitud almacenados como propiedades. El contenido del campo message
en el cuerpo de la solicitud está disponible en la propiedad req.body.message
. El ejemplo simplemente envía este campo al cliente a través de la llamada res.send(req.body.message)
.
Recuerde que los cambios que realice solo tendrán efecto después de reiniciar el servidor. Debido a que está ejecutando el servidor desde una ventana de terminal durante los ejemplos de este capítulo, puede apagar el servidor presionando kbd:[Ctrl+C] en ese terminal. Luego, vuelva a ejecutar el servidor a través del comando node index.js
. La respuesta obtenida por el cliente a la solicitud curl
que mostramos antes ahora es exitosa:
$ curl http://myserver:8080/echo --data message="This is the POST request body" This is the POST request body
Otras formas de enviar y recibir información en una solicitud GET
Puede resultar excesivo utilizar el método HTTP POST
si solo se envían mensajes de texto cortos como el que se utiliza en el ejemplo. En tales casos, los datos se pueden enviar en una query string que comienza con un signo de interrogación. Por lo tanto, la cadena ?message=This+is+the+message
podría incluirse dentro de la ruta de solicitud del método HTTP GET
. Los campos utilizados en la cadena de consulta están disponibles para el servidor en la propiedad req.query
. Por lo tanto, un campo llamado message
está disponible en la propiedad req.query.message
.
Otra forma de enviar datos a través del método HTTP GET
es usar los parámetros de ruta de Express:
app.get('/echo/:message', (req, res) => {
res.send(req.params.message)
})
La ruta en este ejemplo coincide con las solicitudes realizadas con el método GET
usando la ruta /echo/:message
, donde :message
es un marcador de posición para cualquier término enviado con esa etiqueta por el cliente. Estos parámetros son accesibles en la propiedad req.params
. Con esta nueva ruta, el cliente puede solicitar la función echo
del servidor de manera más precisa:
$ curl http://myserver:8080/echo/hello hello
En otras situaciones, la información que el servidor necesita para procesar la solicitud no necesita ser proporcionada explícitamente por el cliente. Por ejemplo, el servidor tiene otra forma de recuperar la dirección IP pública del cliente. Esa información está presente en el objeto req
de forma predeterminada, en la propiedad req.ip
:
app.get('/ip', (req, res) => {
res.send(req.ip)
})
Ahora el cliente puede solicitar la ruta /ip
con el método GET
para encontrar su propia dirección IP pública:
$ curl http://myserver:8080/ip 187.34.178.12
El cliente puede modificar otras propiedades del objeto req
, especialmente los encabezados de solicitud disponibles en req.headers
. La propiedad req.headers.user-agent
, por ejemplo, identifica qué programa está realizando la solicitud. Aunque no es una práctica común, el cliente puede cambiar el contenido de este campo, por lo que el servidor no debe usarlo para identificar de manera confiable a un cliente en particular. Es aún más importante validar los datos proporcionados explícitamente por el cliente, para evitar inconsistencias en los límites y formatos que podrían afectar negativamente a la aplicación.
Ajustes a la respuesta
Como hemos visto en ejemplos anteriores, el parámetro res
es responsable de devolver una respuesta al cliente. Además, el objeto res
puede cambiar otros aspectos de la respuesta. Es posible que haya notado que, aunque las respuestas que hemos implementado hasta ahora son breves mensajes de texto sin formato, el encabezado Content-Type
de las respuestas usa text/html; charset=utf-8
. Aunque esto no impide que se acepte la respuesta de texto sin formato, será más correcto si redefinimos este campo en el encabezado de la respuesta a text/plain
con la configuración res.type('text/plain')
.
Otros tipos de ajustes de respuesta implican el uso de cookies, que permiten al servidor identificar a un cliente que ha realizado una solicitud previamente. Las cookies son importantes para funciones avanzadas, como la creación de sesiones privadas que asocian solicitudes a un usuario específico, pero aquí solo veremos un ejemplo simple de cómo usar una cookie para identificar a un cliente que ha accedido previamente al servidor.
Dado el diseño modularizado de Express, la administración de cookies debe instalarse con el comando npm
antes de usarse en el script:
$ npm install cookie-parser
Después de la instalación, la administración de cookies debe incluirse en el script del servidor. La siguiente definición debe incluirse cerca del comienzo del archivo:
const cookieParser = require('cookie-parser')
app.use(cookieParser())
Para ilustrar el uso de cookies, modifiquemos la función del controlador de la ruta con la ruta raíz /
que ya existe en el script. El objeto req
tiene una propiedad req.cookies
, donde se guardan las cookies enviadas en el encabezado de la solicitud. El objeto res
, por otro lado, tiene un método res.cookie()
que crea una nueva cookie para ser enviada al cliente. La función de controlador en el siguiente ejemplo verifica si existe una cookie con el nombre known
en la solicitud. Si dicha cookie no existe, el servidor asume que se trata de un visitante por primera vez y le envía una cookie con ese nombre a través de la llamada res.cookie('known', '1')
. Asignamos arbitrariamente el valor 1
a la cookie porque se supone que tiene algún contenido, pero el servidor no consulta ese valor. Esta aplicación solo asume que la simple presencia de la cookie indica que el cliente ya ha solicitado esta ruta 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');
})
De forma predeterminada, curl
no utiliza cookies en las transacciones. Pero tiene opciones para almacenar (-c cookies.txt
) y enviar cookies almacenadas (-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!
Debido a que este comando fue el primer acceso desde que se implementaron las cookies en el servidor, el cliente no tenía ninguna cookie para incluir en la solicitud. Como se esperaba, el servidor no identificó la cookie en la solicitud y, por lo tanto, incluyó la cookie en los encabezados de respuesta, como se indica en la línea de la salida del archivo Set-Cookie: known=1; Path=/
. Dado que hemos habilitado las cookies en curl
, una nueva solicitud incluirá la cookie known=1
en los encabezados de la solicitud, lo que permitirá al servidor identificar la presencia de la 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
Seguridad de las cookies
El desarrollador debe ser consciente de las posibles vulnerabilidades al utilizar cookies para identificar a los clientes que realizan solicitudes. Los atacantes pueden utilizar técnicas como cross-site scripting (XSS) y cross-site request forgery (CSRF) para robar cookies de un cliente y, por lo tanto, hacerse pasar por ellos al realizar una solicitud al servidor. En términos generales, estos tipos de ataques utilizan campos de comentarios no validados o URL construidas meticulosamente para insertar código JavaScript malicioso en la página. Cuando es ejecutado por un cliente auténtico, este código puede copiar cookies válidas y almacenarlas o reenviarlas a otro destino.
Por lo tanto, especialmente en aplicaciones profesionales, es importante instalar y utilizar funciones Express más especializadas, conocidas como middleware. El módulo express-session
o cookie-session
proporciona un control más completo y seguro sobre la gestión de la sesión y las cookies. Estos componentes permiten controles adicionales para evitar que las cookies se desvíen de su emisor original.
Ejercicios guiados
-
¿Cómo se puede leer el contenido del campo
comment
, enviado dentro de una cadena de consulta del método HTTPGET
, en una función de controlador? -
Escriba una ruta que utilice el método HTTP
GET
y la ruta/agent
para enviar de vuelta al cliente el contenido del encabezadouser-agent
. -
Express.js tiene una característica llamada route parameters, donde una ruta como
/user/:name
se puede usar para recibir el parámetroname
enviado por el cliente. ¿Cómo se puede acceder al parámetroname
dentro de la función de controlador de la ruta?
Ejercicios de exploración
-
Si el nombre de host de un servidor es
myserver
, ¿qué ruta Express recibiría el envío en el siguiente formulario?<form action="/contact/feedback" method="post"> ... </form>
-
Durante el desarrollo del servidor, el programador no puede leer la propiedad
req.body
, incluso después de verificar que el cliente está enviando correctamente el contenido a través del método HTTPPOST
. ¿Cuál es la causa probable de este problema? -
¿Qué sucede cuando el servidor tiene una ruta establecida en
/user/:name
y el cliente realiza una solicitud a/user/
?
Resumen
Esta lección explica cómo escribir scripts Express para recibir y manejar solicitudes HTTP. Express utiliza el concepto de routes para definir los recursos disponibles para los clientes, lo que le brinda una gran flexibilidad para construir servidores para cualquier tipo de aplicación web. Esta lección abarca los siguientes conceptos y procedimientos:
-
Rutas que utilizan los métodos HTTP
GET
y HTTPPOST
. -
Cómo se almacenan los datos del formulario en el objeto
request
. -
Cómo utilizar los parámetros de ruta.
-
Personalización de encabezados de respuesta.
-
Gestión básica de cookies.
Respuestas a los ejercicios guiados
-
¿Cómo se puede leer el contenido del campo
comment
, enviado dentro de una cadena de consulta del método HTTPGET
, en una función de controlador?El campo
comment
está disponible en la propiedadreq.query.comment
. -
Escriba una ruta que utilice el método HTTP
GET
y la ruta/agent
para devolver al cliente el contenido del encabezadouser-agent
.app.get('/agent', (req, res) => { res.send(req.headers.user-agent) })
-
Express.js tiene una característica llamada route parameters, donde una ruta como
/user/:name
se puede usar para recibir el parámetroname
enviado por el cliente. ¿Cómo se puede acceder al parámetroname
dentro de la función de controlador de la ruta?El parámetro
name
es accesible en la propiedadreq.params.name
.
Respuestas a los ejercicios de exploración
-
Si el nombre de host de un servidor es
myserver
, ¿qué ruta Express recibiría el envío en el siguiente formulario?<form action="/contact/feedback" method="post"> ... </form>
app.post('/contact/feedback', (req, res) => { ... })
-
Durante el desarrollo del servidor, el programador no puede leer la propiedad
req.body
, incluso después de verificar que el cliente está enviando correctamente el contenido a través del método HTTPPOST
. ¿Cuál es la causa probable de este problema?El programador no incluyó el módulo
express.urlencoded
, que permite a Express extraer el cuerpo de una solicitud. -
¿Qué sucede cuando el servidor tiene una ruta establecida en
/user/:name
y el cliente realiza una solicitud a/user/
?El servidor emitirá una respuesta
404 Not Found
, porque la ruta requiere que el cliente proporcione el parámetro:name
.