a
Node.js y Express
En esta parte, nuestro enfoque se desplaza hacia el backend: es decir, hacia la implementación de la funcionalidad en el lado del servidor.
Construiremos nuestro backend sobre NodeJS, que es un entorno de ejecución basado en JavaScript y en el motor Chrome V8 de Google.
Este material del curso fue escrito con la versión v20.11.0 de Node.js. Asegúrate de que tu versión de Node sea al menos tan nueva como la versión utilizada en el material (puedes verificar la versión ejecutando node -v en la línea de comando).
Como se mencionó en la parte 1, los navegadores aún no son compatibles con las funciones más nuevas de JavaScript, y es por eso que el código que se ejecuta en el navegador debe transpilarse con, por ejemplo, babel. La situación con JavaScript ejecutándose en el backend es diferente. La versión más reciente de Node es compatible con la gran mayoría de las funciones más recientes de JavaScript, por lo que podemos usar las funciones más recientes sin tener que transpilar nuestro código.
Nuestro objetivo es implementar un backend que funcione con la aplicación de notas de la parte 2. Sin embargo, comencemos con lo básico implementando una aplicación clásica de "hola mundo".
Ten en cuenta que las aplicaciones y ejercicios de esta parte no son todas aplicaciones de React, y no usaremos la utilidad create vite@latest -- --template react para inicializar el proyecto para esta aplicación.
Ya habíamos mencionado npm en la parte 2, que es una herramienta utilizada para administrar paquetes de JavaScript. De hecho, npm se origina en el ecosistema Node.
Naveguemos a un directorio apropiado y creemos una nueva plantilla para nuestra aplicación con el comando npm init. Responderemos a las preguntas presentadas por la utilidad y el resultado será un archivo package.json generado automáticamente en la raíz del proyecto, que contiene información sobre el proyecto.
{
"name": "backend",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Matti Luukkainen",
"license": "MIT"
}
El archivo define, por ejemplo, que el punto de entrada de la aplicación es el archivo index.js.
Hagamos un pequeño cambio en el objeto scripts agregando un nuevo comando de script:
{
// ...
"scripts": {
"start": "node index.js", "test": "echo \"Error: no test specified\" && exit 1"
},
// ...
}
A continuación, creemos la primera versión de nuestra aplicación agregando un archivo index.js a la raíz del proyecto con el siguiente código:
console.log('hello world')
Podemos ejecutar el programa directamente con Node desde la línea de comando:
node index.js
O podemos ejecutarlo como un script npm:
npm start
El script npm start funciona porque lo definimos en el archivo package.json:
{
// ...
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
// ...
}
Aunque la ejecución del proyecto funciona cuando se inicia llamando a node index.js desde la línea de comando, es habitual que los proyectos npm ejecuten estas tareas como scripts npm.
De forma predeterminada, el archivo package.json también define otro script npm de uso común llamado npm test. Dado que nuestro proyecto aún no tiene una librería de testing, el comando npm test simplemente ejecuta el siguiente comando:
echo "Error: no test specified" && exit 1
Servidor web simple
Cambiemos la aplicación para que sea un servidor web al editar el archivo index.js de la siguiente manera:
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}`)
Una vez que la aplicación se está ejecutando, el siguiente mensaje se imprime en la consola:
Server running on port 3001
Podemos abrir nuestra humilde aplicación en el navegador visitando la dirección http://localhost:3001:
De hecho, el servidor funciona de la misma manera independientemente de la última parte de la URL. Además, la dirección http://localhost:3001/foo/bar mostrará el mismo contenido.
NB Si el puerto 3001 ya está siendo utilizado por alguna otra aplicación, al iniciar el servidor aparecerá el siguiente mensaje de error:
➜ 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)
Tienes dos opciones. Apaga la aplicación usando el puerto 3001 (el JSON Server en la última parte del material estaba usando el puerto 3001), o usa un puerto diferente para esta aplicación.
Echemos un vistazo más de cerca a la primera línea del código:
const http = require('http')
En la primera linea, la aplicación importa el módulo de servidor web integrado de Node. Esto es prácticamente lo que ya hemos estado haciendo en nuestro código del lado del navegador, pero con una sintaxis ligeramente diferente:
import http from 'http'
En estos días, el código que se ejecuta en el navegador utiliza módulos ES6. Los módulos se definen con un export y se utilizan con un import.
Node.js usa módulos CommonJS. La razón de esto es que el ecosistema de Node necesitaba módulos mucho antes de que JavaScript los admitiera en la especificación del lenguaje. Actualmente, Node es compatible con los módulos ES6, pero ya que la compatibilidad aún no es del todo perfecta continuaremos con módulos CommonJS.
Los módulos de CommonJS funcionan casi exactamente como los módulos de ES6, al menos en lo que respecta a nuestras necesidades en este curso.
El siguiente fragmento de nuestro código se ve así:
const app = http.createServer((request, response) => {
response.writeHead(200, { 'Content-Type': 'text/plain' })
response.end('Hello World')
})
El código usa el método createServer del módulo http para crear un nuevo servidor web. Se registra un controlador de eventos en el servidor, que se llama cada vez que se realiza una solicitud HTTP a la dirección del servidor http://localhost:3001.
La solicitud se responde con el código de estado 200, con el cabecera Content-Type establecido en text/plain, y el contenido del sitio que se devolverá establecido en Hello World.
Las últimas filas enlazan el servidor http asignado a la variable app, para escuchar las solicitudes HTTP enviadas al puerto 3001:
const PORT = 3001
app.listen(PORT)
console.log(`Server running on port ${PORT}`)
El propósito principal del servidor backend en este curso es ofrecer datos sin procesar en formato JSON al frontend. Por esta razón, cambiemos inmediatamente nuestro servidor para devolver una lista codificada de notas en 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}`)
Reiniciemos el servidor (puedes apagar el servidor presionando Ctrl+C en la consola) y refresquemos el navegador.
El valor application/json en la cabecera Content-Type informa al receptor que los datos están en formato JSON. El array notes se transforma en un string con formato JSON con el método JSON.stringify(notes). Esto es necesario ya que el metodo response.end() espera un string o un buffer para enviar como el cuerpo de la respuesta.
Cuando abrimos el navegador, el formato que se muestra es exactamente el mismo que en la parte 2, donde usamos json-server para servir la lista de notas:
Express
Es posible implementar nuestro código de servidor directamente con el servidor web http integrado de Node. Sin embargo, es engorroso, especialmente una vez que la aplicación aumenta de tamaño.
Se han desarrollado muchas librerías para facilitar el desarrollo del lado del servidor con Node, al ofrecer una interfaz más agradable para trabajar con el módulo http integrado. Estas librerías tienen como objetivo proporcionar una mejor abstracción para los casos de uso general que generalmente requerimos para construir un servidor backend. Por lejos, la librería más popular destinada a este propósito es Express.
Usemos Express definiéndolo como una dependencia del proyecto con el comando:
npm install express
La dependencia también se agrega a nuestro archivo package.json:
{
// ...
"dependencies": {
"express": "^4.18.2"
}
}
El código fuente de la dependencia se instala en el directorio node_modules ubicado en la raíz del proyecto. Además de Express, puedes encontrar una gran cantidad de otras dependencias en el directorio:
Estas son, de hecho, las dependencias de la librería Express y las dependencias de todas sus dependencias, etc. Estas son las dependencias transitivas de nuestro proyecto.
La versión 4.18.2 de Express se instaló en nuestro proyecto. ¿Qué significa el signo de intercalación delante del número de versión en package.json?
"express": "^4.18.2"
El modelo de control de versiones utilizado en npm se denomina control de versiones semántico.
El signo de intercalación al frente de ^4.18.2 significa que si y cuando se actualizan las dependencias de un proyecto, la versión de Express que se instala será al menos 4.18.2. Sin embargo, la versión instalada de Express también puede ser una que tenga un número de parche más grande (el último número) o un número menor más grande (el número del medio). La versión principal de la librería indicada por el primer número mayor debe ser la misma.
Podemos actualizar las dependencias del proyecto con el comando:
npm update
Asimismo, si empezamos a trabajar en el proyecto en otra computadora, podemos instalar todas las dependencias actualizadas del proyecto definidas en package.json con el comando:
npm install
Si el número mayor de una dependencia no cambia, las versiones más nuevas deberían ser compatibles con versiones anteriores. Esto significa que si nuestra aplicación usara la versión 4.99.175 de Express en el futuro, entonces todo el código implementado en esta parte aún tendría que funcionar sin realizar cambios en el código. Por el contrario, la futura versión 5.0.0. de Express puede contener cambios que provocarían que nuestra aplicación dejara de funcionar.
Web y Express
Volvamos a nuestra aplicación y realicemos los siguientes cambios:
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 poder utilizar la nueva versión de nuestra aplicación, primero tenemos que reiniciarla.
La aplicación no cambió mucho. Justo al comienzo de nuestro código estamos importando express, que esta vez es una función que se usa para crear una aplicación Express almacenada en la variable app:
const express = require('express')
const app = express()
A continuación, definimos dos rutas a la aplicación. El primero define un controlador de eventos, que se utiliza para manejar las solicitudes HTTP GET realizadas a la raíz / de la aplicación:
app.get('/', (request, response) => {
response.send('<h1>Hello World!</h1>')
})
La función del controlador de eventos acepta dos parámetros. El primer parámetro request contiene toda la información de la solicitud HTTP y el segundo parámetro response se utiliza para definir cómo se responde a la solicitud.
En nuestro código, la solicitud se responde utilizando el método send del objeto response. Llamar al método hace que el servidor responda a la solicitud HTTP enviando una respuesta que contiene el string <h1>Hello World!</h1>
, que se pasó al método send. Dado que el parámetro es un string, Express establece automáticamente el valor de la cabecera Content-Type en text/html. El código de estado de la respuesta predeterminado es 200.
Podemos verificar esto desde la pestaña Network en las herramientas para desarrolladores:
La segunda ruta define un controlador de eventos, que maneja las solicitudes HTTP GET realizadas a la ruta notes de la aplicación:
app.get('/api/notes', (request, response) => {
response.json(notes)
})
La solicitud se responde con el método json del objeto response. Llamar al método enviará el array notes que se le pasó como un string con formato JSON. Express establece automáticamente la cabecera Content-Type con el valor apropiado de application/json.
A continuación, echemos un vistazo rápido a los datos enviados en formato JSON.
En la versión anterior donde solo usábamos Node, teníamos que transformar los datos a un string conf formato JSON con el método JSON.stringify:
response.end(JSON.stringify(notes))
Con Express, esto ya no es necesario, porque esta transformación ocurre automáticamente.
Vale la pena señalar queJSON es una cadena y no un objeto JavaScript como el valor asignado a notes.
El experimento que se muestra a continuación ilustra este punto:
El experimento anterior se realizó en el node-repl interactivo. Puedes iniciar el node-repl interactivo escribiendo node en la línea de comando. repl es particularmente útil para probar cómo funcionan los comandos mientras escribes el código de la aplicación. ¡Lo recomiendo mucho!
nodemon
Si hacemos cambios en el código de la aplicación, tenemos que reiniciar la aplicación para ver los cambios. Reiniciamos la aplicación cerrándola primero escribiendo Ctrl+C y luego reiniciando la aplicación. En comparación con el conveniente flujo de trabajo en React, donde el navegador se recarga automáticamente después de realizar los cambios, esto se siente un poco engorroso.
La solución a este problema es nodemon:
nodemon observará los archivos en el directorio en el que se inició nodemon, y si algún archivo cambia, nodemon reiniciará automáticamente tu aplicación de node.
Instalemos nodemon definiéndolo como una dependencia de desarrollo con el comando:
npm install --save-dev nodemon
El contenido de package.json también ha cambiado:
{
//...
"dependencies": {
"express": "^4.18.2",
},
"devDependencies": {
"nodemon": "^3.0.3"
}
}
Si accidentalmente utilizaste el comando incorrecto y la dependencia de nodemon se agregó en "dependencias" en lugar de "devDependencies", cambia manualmente el contenido de package.json para que coincida con lo que se muestra arriba.
Por dependencias de desarrollo, nos referimos a herramientas que son necesarias solo durante el desarrollo de la aplicación, por ejemplo, para probar o reiniciar automáticamente la aplicación, como nodemon.
Estas dependencias de desarrollo no son necesarias cuando la aplicación se ejecuta en modo de producción en el servidor de producción (por ejemplo, Fly.io o Heroku).
Podemos iniciar nuestra aplicación con nodemon así:
node_modules/.bin/nodemon index.js
Los cambios en el código de la aplicación ahora hacen que el servidor se reinicie automáticamente. Vale la pena señalar que, aunque el servidor backend se reinicia automáticamente, el navegador aún debe actualizarse manualmente. Esto se debe a que, a diferencia de cuando se trabaja en React, ni siquiera podemos tener la funcionalidad de recarga en caliente necesaria para recargar automáticamente el navegador.
El comando es largo y bastante desagradable, así que definamos un script npm dedicado para él en el archivo package.json:
{
// ..
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js", "test": "echo \"Error: no test specified\" && exit 1"
},
// ..
}
En el script no es necesario especificar la ruta node_modules/.bin/nodemon a nodemon, porque npm automáticamente sabe buscar el archivo desde ese directorio.
Ahora podemos iniciar el servidor en el modo de desarrollo con el comando:
npm run dev
A diferencia de los scripts de start y test, también tenemos que agregar run al comando ya que se trata de un script no nativo.
REST
Ampliemos nuestra aplicación para que proporcione la API HTTP RESTful como json-server.
Representational State Transfer, también conocido como REST, se introdujo en 2000 en la disertación de Roy Fielding . REST es un estilo arquitectónico destinado a crear aplicaciones web escalables.
No vamos a profundizar en la definición de REST de Fielding ni a perder tiempo reflexionando sobre qué es y qué no es REST. En cambio, tomamos una visión más estrecha al preocuparnos solo por cómo las API RESTful se entienden generalmente en las aplicaciones web. De hecho, la definición original de REST ni siquiera se limita a las aplicaciones web.
Mencionamos en la parte anterior que las cosas singulares, como las notas en el caso de nuestra aplicación, se llaman recursos en el pensamiento REST. Cada recurso tiene una URL asociada que es la dirección única del recurso.
Una convención es crear la dirección única para los recursos combinando el nombre del tipo de recurso con el identificador único del recurso.
Supongamos que la URL raíz de nuestro servicio eswww.example.com/api.
Si definimos el tipo de recurso de notas a ser note, entonces la dirección de un recurso de nota con el identificador 10, tiene la dirección única www.example.com/api/notes/10.
La URL de la colección completa de todos los recursos de notas es www.example.com/api/notes.
Podemos ejecutar diferentes operaciones sobre recursos. La operación a ejecutar está definida por el verbo HTTP:
URL | verbo | funcionalidad |
---|---|---|
notes/10 | GET | obtiene un solo recurso |
notes | GET | obtiene todos los recursos en una colección |
notes | POST | crea un nuevo recurso basado en los datos de la solicitud |
notes/10 | DELETE | elimina el recurso identificado |
notes/10 | PUT | reemplaza todo el recurso identificado con los datos de la solicitud |
notes/10 | PATCH | reemplaza una parte del recurso identificado con los datos de la solicitud |
Así es como logramos definir aproximadamente a qué se refiere REST como una interfaz uniforme, lo que significa una forma consistente de definir interfaces que hace posible que los sistemas cooperen.
Esta forma de interpretar REST cae dentro del segundo nivel de madurez RESTful en el Modelo de Madurez de Richardson. Según la definición proporcionada por Roy Fielding, en realidad no hemos definido una API REST. De hecho, una gran mayoría de las API "REST" supuestas del mundo no cumplen con los criterios originales de Fielding descritos en su disertación.
En algunos lugares (ver por ejemplo, Richardson, Ruby: RESTful Web Services ) verá nuestro modelo para una API CRUD sencilla, que se conoce como un ejemplo de arquitectura orientada a recursos en lugar de REST. Evitaremos quedarnos atascados discutiendo semántica y en su lugar volveremos a trabajar en nuestra aplicación.
Obteniendo un solo recurso
Ampliemos nuestra aplicación para que ofrezca una interfaz REST para operar con notas individuales. Primero, creemos una ruta para buscar un solo recurso.
La dirección única que usaremos para una nota individual es de la forma notes/10, donde el número al final se refiere al número de id único de la nota.
Podemos definir parámetros para rutas en Express usando la sintaxis de dos puntos:
app.get('/api/notes/:id', (request, response) => {
const id = request.params.id
const note = notes.find(note => note.id === id)
response.json(note)
})
Ahora app.get('/api/notes/:id', ...)
manejará todas las solicitudes HTTP GET, que tienen el formato /api/notes/SOMETHING, donde SOMETHING es una cadena arbitraria.
Se puede acceder al parámetro id en la ruta de una solicitud a través del objeto request:
const id = request.params.id
El ahora familiar método find de arrays se utiliza para encontrar la nota con un id que coincida con el parámetro. Luego, la nota se devuelve al remitente de la solicitud.
Cuando probamos nuestra aplicación yendo a http://localhost:3001/api/notes/1 en nuestro navegador, notamos que no parece funcionar, ya que el navegador muestra una página vacía. Esto no nos sorprende como desarrolladores de software, y es hora de depurar.
Agregar comandos de console.log a nuestro código es un viejo truco comprobado en batalla:
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)
})
Cuando visitemos http://localhost:3001/api/notes/1 nuevamente en el navegador, la consola que es el terminal en este caso, mostrará lo siguiente:
El parámetro id de la ruta se pasa a nuestra aplicación, pero el método find no encuentra una nota con ese id.
Para profundizar nuestra investigación, también agregamos un console log dentro de la función de comparación pasada al método find. Para hacer esto, tenemos que deshacernos de la sintaxis de la función de flecha compacta note => note.id === id, y usar la sintaxis con una declaración de retorno explícita:
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)
})
Cuando volvemos a visitar la URL en el navegador, cada llamada a la función de comparación imprime algunas cosas diferentes en la consola. La salida de la consola es la siguiente:
1 'number' '1' 'string' false 2 'number' '1' 'string' false 3 'number' '1' 'string' false
La causa del error se aclara. La variable id contiene una cadena '1', mientras que los ids de las notas son números enteros. En JavaScript, la comparación "triple iguales" === considera que todos los valores de diferentes tipos no son iguales por defecto, lo que significa que 1 no es '1'.
Solucionemos el problema cambiando el parámetro id de un string a 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)
})
Ahora la búsqueda de un recurso individual funciona.
Sin embargo, hay otro problema con nuestra aplicación.
Si buscamos una nota con un id que no existe, el servidor responde con:
El código de estado HTTP que se devuelve es 200, lo que significa que la respuesta se realizó correctamente. No se devuelven datos con la respuesta, ya que el valor de la cabecera de content-length es 0, y lo mismo se puede verificar desde el navegador.
El motivo de este comportamiento es que la variable note se establece en undefined si no se encuentra una nota coincidente. La situación debe manejarse en el servidor de una mejor manera. Si no se encuentra ninguna nota, el servidor debe responder con el código de estado 404 not found en lugar de 200.
Hagamos el siguiente cambio en nuestro 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() }})
Dado que no se adjuntan datos a la respuesta, utilizamos el método status para establecer el estado y el método end para responder a la solicitud sin enviar ningún dato.
La condición if aprovecha el hecho de que todos los objetos JavaScript son truthy, lo que significa que se evalúan como verdaderos en una operación de comparación. Sin embargo, undefined es falsy, lo que significa que se evaluará como falso.
Nuestra aplicación funciona y envía el código de estado de error si no se encuentra ninguna nota. Sin embargo, la aplicación no devuelve nada para mostrar al usuario, como suelen hacer las aplicaciones web cuando visitamos una página que no existe. En realidad, no necesitamos mostrar nada en el navegador porque las API REST son interfaces diseñadas para uso programático, y el código de estado de error es todo lo que se necesita.
De todos modos, es posible dar una pista sobre la razón de enviar un error 404 al sobrescribir el mensaje predeterminado de NO ENCONTRADO.
Eliminar recursos
A continuación, implementemos una ruta para eliminar recursos. La eliminación ocurre al realizar una solicitud HTTP DELETE a la URL del 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()
})
Si la eliminación del recurso es exitosa, lo que significa que la nota existe y se elimina, respondemos a la solicitud con el código de estado 204 no content y no devolvemos datos con la respuesta.
No hay consenso sobre qué código de estado debe devolverse a una solicitud DELETE si el recurso no existe. Realmente, las únicas dos opciones son 204 y 404. En aras de la simplicidad, nuestra aplicación responderá con 204 en ambos casos.
Postman
Entonces, ¿cómo probamos la operación de eliminación? Las solicitudes HTTP GET son fáciles de realizar desde el navegador. Podríamos escribir algo de JavaScript para probar la eliminación, pero escribir código de prueba no siempre es la mejor solución en todas las situaciones.
Existen muchas herramientas para facilitar las pruebas de backends. Uno de ellos es un programa de línea de comandos curl. Sin embargo, en lugar de curl, analizaremos el uso de Postman para probar la aplicación.
Instalemos Postman y probémoslo:
NB: Postman también está disponible en VS Code y se puede descargar desde la pestaña Extensiones a la izquierda -> buscar Postman -> Primer resultado (Editor verificado) -> Instalar. Luego verás un icono adicional agregado en la barra de actividades debajo de la pestaña de extensiones. Una vez que inicies sesión, puedes seguir los pasos a continuación.
Usar Postman es bastante fácil en esta situación. Es suficiente definir la URL y luego seleccionar el tipo de solicitud correcto (DELETE).
El servidor backend parece responder correctamente. Al realizar una solicitud HTTP GET a http://localhost:3001/api/notes, vemos que la nota con el id 2 ya no está en la lista, lo que indica que la eliminación fue exitosa.
Debido a que las notas de la aplicación solo se guardan en la memoria, la lista de notas volverá a su estado original cuando reiniciemos la aplicación.
El cliente REST de Visual Studio Code
Si usas Visual Studio Code, puedes usar el plugin REST client de VS Code en lugar de Postman.
Una vez que el plugin está instalado, usarlo es muy simple. Creamos un directorio en la raíz de la aplicación llamada requests. Guardamos todas las solicitudes del cliente REST en el directorio como archivos que terminan con la extensión .rest.
Creemos un nuevo archivo get_all_notes.rest y definamos la solicitud que obtiene todas las notas.
Al hacer clic en el texto Send Request, el cliente REST ejecutará la solicitud HTTP y la respuesta del servidor se abre en el editor.
El Cliente HTTP de WebStorm
Si usas IntelliJ WebStorm en cambio, puedes usar un procedimiento similar con su Cliente HTTP incorporado. Crea un nuevo archivo con la extensión .rest
y el editor te mostrará opciones para crear y ejecutar tus solicitudes. Puedes obtener más información al respecto siguiendo esta guía.
Recibiendo información
A continuación, hagamos posible agregar nuevas notas al servidor. La adición de una nota ocurre al hacer una solicitud HTTP POST a la dirección http://localhost:3001/api/notes, y al enviar toda la información de la nueva nota en el body de la solicitud en formato JSON.
Para acceder a los datos fácilmente, necesitamos la ayuda del json-parser de Express, que se usa con el comando app.use(express.json()).
Activemos json-parser e implementemos un controlador inicial para manejar las solicitudes 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)})
La función del controlador de eventos puede acceder a los datos de la propiedad body del objeto request.
Sin json-parser, la propiedad body no estaría definida. El json-parser funciona para que tome los datos JSON de una solicitud, los transforme en un objeto JavaScript y luego los adjunte a la propiedad body del objeto request antes de llamar al controlador de ruta.
Por el momento, la aplicación no hace nada con los datos recibidos además de imprimirlos a la consola y devolverlos en la respuesta.
Antes de implementar el resto de la lógica de la aplicación, verifiquemos con Postman que el servidor realmente recibe los datos. Además de definir la URL y el tipo de solicitud en Postman, también tenemos que definir los datos enviados en body:
La aplicación imprime los datos que enviamos en la solicitud a la consola:
NB Mantén visible el terminal que ejecuta la aplicación en todo momento cuando trabajes en el backend. Gracias a Nodemon, cualquier cambio que hagamos en el código reiniciará la aplicación. Si prestas atención a la consola, inmediatamente podrás detectar los errores que ocurren en la aplicación:
De manera similar, es útil verificar la consola para asegurarnos de que el backend se comporte como esperamos en diferentes situaciones, como cuando enviamos datos con una solicitud HTTP POST. Naturalmente, es una buena idea agregar muchos comandos console.log al código mientras la aplicación aún se está desarrollando.
Una posible causa de problemas es un cabecera Content-Type configurado incorrectamente en las solicitudes. Esto puede suceder con Postman si el tipo de body no está definido correctamente:
La cabecera Content-Type se establece en text/plain:
El servidor parece recibir solo un objeto vacío:
El servidor no podrá parsear los datos correctamente sin el valor correcto en la cabecera. Ni siquiera intentará adivinar el formato de los datos, ya que hay una gran cantidad de Content-Types potenciales.
Si utilizas VS Code, deberías instalar ahora el cliente REST del capítulo anterior, si aún no lo has hecho. La solicitud POST se puede enviar con el cliente REST de esta manera:
Creamos un nuevo archivo create_note.rest para la solicitud. La solicitud se formatea de acuerdo con las instrucciones de la documentación.
Un beneficio que tiene el cliente REST sobre Postman es que las solicitudes están fácilmente disponibles en la raíz del repositorio del proyecto y se pueden distribuir a todos en el equipo de desarrollo. También puedes agregar varias solicitudes en el mismo archivo usando ###
como 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"
}
Postman también permite a los usuarios guardar solicitudes, pero la situación puede volverse bastante caótica, especialmente cuando se trabaja en varios proyectos no relacionados.
Nota al margen importante
A veces, cuando estas depurando, es posible que desees averiguar qué cabeceras se han configurado en la solicitud HTTP. Una forma de lograr esto es mediante el método get del objeto request, que se puede usar para obtener el valor de una sola cabecera. El objeto request también tiene la propiedad headers (cabeceras), que contiene todas los cabeceras de una solicitud específica.
Pueden ocurrir problemas con el cliente VS REST si agrega accidentalmente una línea vacía entre la fila superior y la fila que especifica los cabeceras HTTP. En esta situación, el cliente REST interpreta que esto significa que todos los cabeceras se dejan vacíos, lo que hace que el servidor backend no sepa que los datos que ha recibido están en formato JSON.
Podrás identificar la ausencia de la cabecera Content-Type si en algún momento en tu código imprimes todas las cabeceras de la solicitud con el comando console.log(request.headers).
Volvamos a la aplicación. Una vez que sabemos que la aplicación recibe los datos correctamente, es el momento de finalizar el manejo de la solicitud:
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)
})
Necesitamos un id única para la nota. Primero, encontramos el número de id más grande en la lista actual y lo asignamos a la variable maxId. La id de la nueva nota se define como maxId + 1. De hecho, este método no se recomienda, pero lo haremos así por ahora, ya que lo reemplazaremos pronto.
La versión actual todavía tiene el problema de que la solicitud HTTP POST se puede usar para agregar objetos con propiedades arbitrarias. Mejoremos la aplicación definiendo que la propiedad content no puede estar vacía. La propiedad important recibirá el valore predeterminado false. Todas las demás propiedades se descartan:
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: Boolean(body.important) || false,
id: generateId(),
}
notes = notes.concat(note)
response.json(note)
})
La lógica para generar el nuevo número de id para notas se ha separado en una función generateId.
Si a los datos recibidos les falta un valor para la propiedad content, el servidor responderá a la solicitud con el código de estado 400 bad request:
if (!body.content) {
return response.status(400).json({
error: 'content missing'
})
}
Ten en cuenta que llamar a return es crucial, porque de lo contrario el código se ejecutará hasta el final y la nota con formato incorrecto se guardará en la aplicación.
Si la propiedad content tiene un valor, la nota se basará en los datos recibidos. Si falta la propiedad important, el valor predeterminado será false. El valor predeterminado se genera actualmente de una manera bastante extraña:
important: Boolean(body.important) || false,
Si los datos guardados en la variable body tienen la propiedad important, la expresión evaluará su valor. Si la propiedad no existe, la expresión se evaluará como falsa, que se define en el lado derecho de las líneas verticales.
Para ser exactos, cuando la propiedad important es false, entonces la expresión body.important || false devolverá el false del lado derecho ...
Puedes encontrar el código para nuestra aplicación actual en su totalidad en la rama part3-1 de este repositorio de GitHub.
Si clonas el proyecto, ejecuta el comando npm install antes de iniciar la aplicación con npm start o npm run dev.
Una cosa más antes de pasar a los ejercicios. La función para generar IDs se ve actualmente así:
const generateId = () => {
const maxId = notes.length > 0
? Math.max(...notes.map(n => n.id))
: 0
return maxId + 1
}
El cuerpo de la función contiene una línea que parece un poco intrigante:
Math.max(...notes.map(n => n.id))
¿Qué está sucediendo exactamente en esa línea de código? notes.map(n => n.id) crea un nuevo array que contiene todos los ids de las notas. Math.max devuelve el valor máximo de los números que se le pasan. Sin embargo, notes.map(n => n.id) es un array, por lo que no se puede asignar directamente como parámetro a Math.max. El array se puede transformar en números individuales mediante el uso de la sintaxis de spread (tres puntos) ....
Acerca de los tipos de solicitudes HTTP
El estándar HTTP habla de dos propiedades relacionadas con los tipos de solicitud, segura e idempotente.
La solicitud HTTP GET debe ser segura:
En particular, se ha establecido la convención de que los métodos GET y HEAD NO DEBEN tener el poder de ejecutar una acción que no sea la recuperación. Estos métodos deben considerarse "seguros".
Seguridad significa que la solicitud en ejecución no debe causar ningún efecto secundario en el servidor. Por efectos secundarios queremos decir que el estado de la base de datos no debe cambiar como resultado de la solicitud, y la respuesta solo debe devolver datos que ya existen en el servidor.
Nada puede garantizar que una solicitud GET sea realmente segura; de hecho, esto es solo una recomendación que se define en el estándar HTTP. Al adherirse a los principios RESTful en nuestra API, las solicitudes GET se utilizan siempre de una manera segura.
El estándar HTTP también define el tipo de solicitud HEAD, que debería ser seguro. En la práctica, HEAD debería funcionar exactamente como GET, pero no devuelve nada más que el código de estado y las cabeceras de respuesta. El cuerpo de la respuesta no se devolverá cuando realice una solicitud HEAD.
Todas las solicitudes HTTP excepto POST deben ser idempotentes:
Los métodos también pueden tener la propiedad de "idempotencia" en el sentido de que (aparte de errores o problemas de caducidad) los efectos secundarios de N > 0 solicitudes idénticas son los mismos que para una sola solicitud. Los métodos GET, HEAD, PUT y DELETE comparten esta propiedad
Esto significa que si una solicitud tiene efectos secundarios, el resultado debería ser el mismo independientemente de cuántas veces se envíe la solicitud.
Si hacemos una solicitud HTTP PUT a la url /api/notes/10 y con la solicitud enviamos los datos { content: "no side effects!", important: true }, el resultado es el mismo independientemente de cuántas veces se envía la solicitud.
Al igual que la seguridad para la solicitud GET, la idempotencia también es solo una recomendación en el estándar HTTP y no algo que se pueda garantizar simplemente en función del tipo de solicitud. Sin embargo, cuando nuestra API se adhiere a los principios RESTful, las solicitudes GET, HEAD, PUT y DELETE se utilizan de tal manera que son idempotentes.
POST es el único tipo de solicitud HTTP que no es ni seguro ni idempotente. Si enviamos 5 solicitudes HTTP POST diferentes a /api/notes con un cuerpo de {content: "many same", important: true}, las 5 notas resultantes en el servidor tendrán todas el mismo contenido.
Middleware
El json-parser de Express que utilizamos anteriormente es un middleware.
Los middleware son funciones que se pueden utilizar para manejar objetos de request y response.
El json-parser que usamos anteriormente toma los datos sin procesar de las solicitudes que están almacenadas en el objeto request, los parsea en un objeto de JavaScript y lo asigna al objeto request como una nueva propiedad body.
En la práctica, puedes utilizar varios middleware al mismo tiempo. Cuando tienes más de uno, se ejecutan uno por uno en el orden en el que se definieron en el código de la aplicación.
Implementemos nuestro propio middleware que imprime información sobre cada solicitud que se envía al servidor.
Middleware es una función que recibe tres 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()
}
Al final del cuerpo de la función, se llama a la función next que se pasó como parámetro. La función next cede el control al siguiente middleware.
El middleware se utiliza así:
app.use(requestLogger)
Recuerda, las funciones middleware se llaman en el orden en el que son encontradas por el motor de JavaScript. Ten en cuenta que json-parser se encuentra definido antes que requestLogger, porque de lo contrario, ¡request.body no se inicializará cuando se ejecute el logger!
Las funciones de middleware deben utilizarse antes que las rutas cuando queremos que sean ejecutadas por los controladores de eventos de ruta. A veces, queremos usar funciones de middleware después que las rutas. Hacemos esto cuando las funciones de middleware solo son llamadas si ningún controlador de ruta se encarga de la solicitud HTTP.
Agreguemos el siguiente middleware después de nuestras rutas, que se usa para capturar solicitudes realizadas a rutas inexistentes. Para estas solicitudes, el middleware devolverá un mensaje de error en formato JSON.
const unknownEndpoint = (request, response) => {
response.status(404).send({ error: 'unknown endpoint' })
}
app.use(unknownEndpoint)
Puedes encontrar el código para nuestra aplicación actual en su totalidad en la rama part3-2 de este repositorio de GitHub.