Descarga la aplicación para disfrutar aún más
Vista previa del material en texto
1.1 1.2 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 3.1 3.2 3.3 4.1 4.2 4.3 4.4 Tabla de contenido Portada Introducción ¿Que es un API? Configurando el proyecto con Express JS Inicializando el proyecto Herramientas de desarrollo Crear un simple Web Server Utilizando Express JS Configuración y variables de entorno Middleware para manejo de errores Sistema de Logs Postman Router y Routes en Express JS Utilizando el Router y Routes de Express Creando el layout del API Capturando y procesando parámetros de las peticiones Patrones asincrónicos de JavaScript Patrones asincrónicos de JavaScript Callbacks Promises async / await Persistencia de datos 2 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 5.9 5.10 5.11 5.12 5.13 6.1 6.2 6.3 6.4 6.5 6.6 6.7 7.1 7.2 7.3 7.4 8.1 Bases de datos SQL y NoSQL Instalando y configurando MongoDB Conectando con MongoDB Mongoose Models Procesando parámetros comunes con middleware Estandarización de la respuesta Mongoose Schemas Paginación Ordenamiento Creando recursos Relaciones entre recursos Consultar recursos anidados Añadir recursos anidados Asegurando el API Añadir y remover campos de un documento Administración de usuarios Autenticación de usuarios Autorización de usuarios Validaciones personalizadas Limpieza de campos Restricción de las peticiones Probando el API Pruebas Configurando pruebas Pruebas unitarias Pruebas de integración Documentando el API Configurando Swagger y JSDoc 3 8.2 8.3 Documentando las rutas y creando modelos Conclusión 4 Introducción A quién está dirigido este libro Este libro va dirigido a programadores que han iniciado con Node.js o tienen experiencia en otros lenguajes de Backend como Java, Rails o PHP y quieren utilizar Node.js como componente de Middleware o Backend para hacer un REST API Web. Si eres nuevo en Javascript se recomienda tomar el siguiente curso: Introduction to JavaScript - Code Academy Adicionalmente en este libro se asume conocimientos de los features básicos de ES2015 al menos: Arrow functions let + const Template Strings Destructuring assignment Promises Para mayor información puede consultar el siguiente enlace: Learn ES2015 - Babel Si eres nuevo en Node.js se recomienda leer el siguiente libro de este misma serie: Introducción a Node JS También se asume que el lector tiene conocimientos básicos de comandos en la Terminal y utilización básica de git. Acerca de este libro Este libro tiene un objetivo teórico-práctico, durante el transcurso de este construiremos una aplicación desde el inicio hasta el fin, comenzaremos por lo conceptos básicos y gradualmente en cada capítulo introduciremos nuevos conceptos, fragmentos de código y buenas prácticas, veremos como la aplicación se convierta en un proyecto robusto. Pero a su vez si usted lo desea puede saltar a un capitulo en especifico, por ejemplo para ver el versionamiento de API o la persistencia de datos con MongoDB. Introducción 5 https://www.codecademy.com/learn/introduction-to-javascript https://babeljs.io/learn-es2015/ https://www.gitbook.com/book/gmoralesc/introduccion-a-node-js/details Utilizaremos git como software de control de versiones para organizar cada uno de los pasos incrementales (features) de la aplicación, brindando así la posibilidad de saltar a un paso específico en el tiempo de desarrollo del proyecto y si usted lo desea comenzar desde allí, a la vez nos brinda la ventaja de ver los cambios en los archivos de manera visual y así poder comprobar en caso de una omisión o error. Acerca del autor Soy un entusiasta de la Web desde la década de finales de los 90 cuando se utilizaba el bloc de notas y nombre de etiquetas en mayúsculas para hacer las páginas Web, soy Ingeniero de Sistemas y tengo más de diez años de experiencia como profesor de pregrado en la asignatura de programación bajo la Web y en posgrado con otras asignaturas relacionadas con el mundo de desarrollo de software, trabajo actualmente en la industria del desarrollo de software específicamente la programación Web. Puedes encontrar más información acerca de mi hoja de vida en el siguiente enlace: http://gmoralesc.me. Gustavo Morales Agradecimientos Quiero agradecer a mi esposa Milagro y mi hijo Miguel Angel por apoyarme y entender la gran cantidad de horas invertidas frente al computador: investigando, estudiando, probando y escribiendo. Introducción 6 http://gmoralesc.me ¿Que es un API? Es un acrónimo en inglés de Application Programming Interface, lo cual en otras palabras un pocos menos abstractas, es la interfaz que una aplicación expone para que otras aplicaciones interactúen con sus métodos o servicios, a través de una interfaz consumiendo unas recursos específicos. API Web En nuestro proyecto crearemos una API Web la cual utiliza como interfaz el protocolo Web es cuál es HTTP (o HTTPS sea el caso) y los recursos son las entidades, por ejemplo nosotros vamos a crear un administrador de tareas por lo tanto nuestros recursos serán tareas, usuarios y grupos, entonces nuestras entidades serán: tasks, users y groups, normalmente las entidades se nombran el plural y las cuales se acceden a través de unas rutas en este caso son urls. Se recomienda utilizar el idioma inglés independientemente de la lengua nativa de la aplicación. Existen otros tipos de API por ejemplo en vez del protocolo HTTP pueden ser sockets y en vez de entidades pueden ser canales, este tipo de API son muy utilizadas en juegos en línea, por lo tanto es importante definir que tipo de API estamos construyendo. Códigos y verbos HTTP El protocolo HTTP utiliza diferentes verbos para las diferentes operaciones que se realizan y dependiendo el resultado de estas operaciones vienen con diferentes códigos. Los verbos más utilizados por defecto en la Web son: GET: Se utiliza cuando se envía una solicitud para obtener un recurso por ejemplo un documento HTML a través de una url. OPTIONS: Se utiliza como petición auxiliar para comprobar si el servidor soporta el método que se va a utilizar en general cualquiera diferente a GET, esta comprobación se llama preflight. POST: Se utiliza para crear un recurso, normalmente enviar información de formularios Web. Pero no son los únicos, existen además: PUT, PATCH y DELETE. ¿Que es un API? 7 Dentro del listado de códigos tenemos: 2xx: Este grupo se utiliza para operaciones exitosas. 200: Cuando la operación es exitosa. 201: Lo mismo que el 200 pero en este caso cuando además se crea un recurso de manera exitosa. 204: Lo mismo que el 201 pero esta vez no fue necesario enviar ningún tipo de información de vuelta 3xx: Este grupo se utiliza para indicar redirecciones. 4xx: Este grupo se utiliza para enumerar los errores del lado del cliente. 400: Indica que la petición no fue bien realiza normalmente cuando los datos se envian mal formados. 401: No está autorizado para acceder al recurso, normalmente se utiliza cuando no se está autenticado. 403: Puede que esté "autenticado" pero no tiene permisos para acceder a ese recurso 404: Recurso no encontrado 422: Indica que la petición no se puede procesar debido a que faltan datos requeridos o están en un formato incorrecto. 5xx: Este conjunto asociado a errores del lado del servidor. REST API Web REST es un acrónimo de Representational State Transfer, que especifica un contrato por defecto para la relación que debe haber entre las rutas, verbos, códigos (estos dos anteriores pertenecientes al protocolo HTTP) y operaciones principales como: Crear (Create), Leer (Read), Actualizar (Update) y Borrar (Delete) conocido mucho por su acrónimo CRUD, la siguiente tabla mostrará cómo sería para nuestra entidad tasks: Verbo StatusCode Operación Descripción /api/tasks/ POST 201 CREATE Crear un tarea /api/tasks/ GET 200 READ Leer todos las tareas /api/tasks/:id GET 200 READ Leer una tarea /api/tasks/:id PUT /PATCH 200 UPDATE Actualizar la información de una tarea /api/tasks/:id DELETE 200 (o204) DELETE Eliminar una tarea ¿Que es un API? 8 Estecontrato no es una camisa de fuerza, pero se deben respetar las definiciones básicas, no está permitido por ejemplo crear una tarea con el método DELETE en vez de POST, no tiene mucho sentido, pero se pueden añadir más muchas más operaciones, recursos anidados y demás, se dice que un API es RESTful cuando cumple con este contrato mínimo. ¿Que es un API? 9 Inicialización del proyecto Seleccionamos un directorio de trabajo y creamos el directorio de la aplicación: mkdir checklist-api && cd checklist-api Inicializamos el proyecto de Node JS: npm init -y El comando anterior genera el archivo package.json que luego se puede editar para cambiar los valores de la descripción, autor y demás. Inicializamos el repositorio de git: git init Creamos el archivo .gitignore en la raíz del proyecto: touch .gitignore Con el siguiente contenido y lo guardamos: node_modules/ .DS_Store Thumbs.db Creamos el archivo principal de la aplicación: touch index.js Agregamos el script inicial en el archivo package.json para ejecutar nuestro archivo principal: "scripts": { "start": "node index", "test": "echo \"Error: no test specified\" && exit 1" }, Inicializando el proyecto 10 Para terminar con la configuración de la aplicación hacemos el commit inicial: git add --all git commit -m "Initial commit" Inicializando el proyecto 11 Herramientas de desarrollo Nodemon Es muy tedioso cada vez que realizamos un cambio en los archivos tener que detener e iniciar nuevamente la aplicación, para esto vamos a utilizar una librería llamada nodemon, esta vez como una dependencia solo para desarrollo, ya que no la necesitamos cuando la aplicación esté corriendo en producción, esta librería nos brinda la opción de que después de guardar cualquier archivo en el directorio de trabajo, nuestra aplicación se vuelva a reiniciar automáticamente. npm install -D nodemon Luego modificamos la sección de scripts del archivo package.json para añadir el script que ejecutará nuestra aplicación en modo de desarrollo y en el script start en modo de producción, de la siguiente manera: "scripts": { "dev": "nodemon", "start": "node index", "test": "echo \"Error: no test specified\" && exit 1" }, No es necesario indicarle a nodemon que archivo va a ejecutar pues por convención buscará el archivo index.js , el cual es el punto de entrada de nuestra aplicación. Mas adelante se configurará el script de pruebas: test Si el servidor se está ejecutando lo detenemos con CTRL+C , pero esta vez lo ejecutamos con npm run dev . Para comprobar que todo esté funcionando, abrimos el navegador en la siguiente dirección http://localhost:3000 . Antes de finalizar hacemos commit de nuestro trabajo: git add --all git commit -m "Add Nodemon for development" Más información: Herramientas de desarrollo 12 Nodemon ESLint Es recomendado añadir una herramienta que nos compruebe la sintaxis de los archivos para evitar posibles errores y porque no, normalizar la forma en que escribimos el código, para ello utilizaremos una librería llamada ESLint que junto con el editor de texto comprobará la sintaxis de los archivos en tiempo real: npm install -D eslint ESLint nos permite seleccionar una guia de estilo ya existente o también poder crear nuestra propia guia, todo depende del común acuerdo del equipo de desarrollo. Inicializamos la configuración con el siguiente comando: ./node_modules/.bin/eslint --init Alternativamente con la utilidad npx , instalada globalmente por Node.js, con el siguiente comando: npx eslint --init Si ESLint está instalado de manera global, el comando anterior se podría reemplazar con: eslint --init Contestamos las preguntas del asistente de configuración de la siguiente manera: ? How would you like to use ESLint? To check syntax only To check syntax and find problems ❯ To check syntax, find problems, and enforce code style ? What type of modules does your project use? JavaScript modules (import/export) ❯ CommonJS (require/exports) None of these Herramientas de desarrollo 13 https://nodemon.io/ ? Which framework does your project use? React Vue.js ❯ None of these ? Where does your code run? ◯ Browser ❯◉ Node ? How would you like to define a style for your project? ❯ Use a popular style guide Answer questions about your style Inspect your JavaScript file(s) ? Which style guide do you want to follow? ❯ Airbnb (https://github.com/airbnb/javascript) Standard (https://github.com/standard/standard) Google (https://github.com/google/eslint-config-google) ? What format do you want your config file to be in? JavaScript YAML ❯ JSON Checking peerDependencies of eslint-config-airbnb-base@latest The config that you've selected requires the following dependencies: eslint-config-airbnb-base@latest eslint@^4.19.1 || ^5.3.0 eslint-plugin-import@^2.14.0 ? Would you like to install them now with npm? (Y/n) Y Una vez finalizado el proceso creará un archivo en la raíz de nuestro proyecto llamado .eslintrc.json , el cual contiene toda la configuración. Adicionalmente vamos a añadir las siguientes excepciones: 1. Vamos a utilizar el objeto global console para imprimir muchos mensajes en la consola y no queremos que lo marque como error. 2. A modo de ejemplo más adelante vamos añadir un parámetro que no utilizaremos inmediatamente pero nos servirá mucho para explicar un concepto el cual es next y no queremos que no los marque como error. Herramientas de desarrollo 14 { "env": { "commonjs": true, "es6": true, "node": true }, "extends": "airbnb-base", "globals": { "Atomics": "readonly", "SharedArrayBuffer": "readonly" }, "parserOptions": { "ecmaVersion": 2018 }, "rules": { "no-console": "off", "no-unused-vars": [ "error", { "argsIgnorePattern": "next" } ] } } Antes de finalizar hacemos commit de nuestro trabajo git add --all git commit -m "Add ESLint for development" Más información: ESLint Visual Studio Code Para añadir nuestra configuración personalizada para el proyecto primero se debe crear el directorio y el archivo de configuración: mkdir .vscode touch .vscode/settings.json Dentro del archivo de configuración settings.json colocamos la siguiente información: Herramientas de desarrollo 15 https://eslint.org/ { "editor.formatOnSave": true, "editor.renderWhitespace": "all", "editor.renderControlCharacters": true, "editor.trimAutoWhitespace": true, "editor.tabSize": 2, "files.insertFinalNewline": true, "files.eol": "\n", "prettier.trailingComma": "es5", "prettier.eslintIntegration": true } Si estás utilizando Visual Studio Code puedes añadir estos parámetros en la configuración: Establecer que siempre se ordene el documento a la hora de guardar el archivo, que muestre todos los espacios en blanco y caracteres especiales y remueva todos los espacios en blanco que sobren. Dejar una línea en blanco al final de cada archivo (requerido por defecto por ESLint) e indicar cuál será el carácter de fin de línea, esto más que todo para retrocompatibilidad con los sistemas Windows. Si tienes el plugin de Prettier para ordenar el documento puedes indicarle que tome las reglas de la configuración ESLint que tienes en el proyecto. Finalmente si deseas añadir una coma al final de algunas estructuras de JavaScript puedes indicarle a prettier que lo haga, esta es una buena práctica cuando se utiliza git y cada línea nueva no necesita añadir una coma en el elemento anterior. Es posible establecer unos plugins sugeridos para que una vez el programador abra el proyecto en Visual Studio Code muestra la sugerencia: touch .vscode/extensions.json Con el siguiente contenido: { "recommendations": [ "waderyan.nodejs-extension-pack", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode" ] } Depuración con Visual Studio Code Crear el archivo de configuración: Herramientas de desarrollo 16 touch .vscode/launch.json Con lasiguiente información: { "version": "0.2.0", "configurations": [ { "type": "node", "request": "attach", "name": "Node: Nodemon", "processId": "${command:PickProcess}", "restart": true, "protocol": "inspector" } ] } Luego modificamos la sección de scripts del archivo package.json para añadir la opción que permitirá hacer debug al nodemon: "scripts": { "dev": "nodemon --inspect", "start": "node index", "test": "echo \"Error: no test specified\" && exit 1" }, Se debe detener el Servidor una vez más para tomar los últimos cambios. Antes de finalizar hacemos commit de nuestro trabajo: git add --all git commit -m "Add config for debugging with VSCode" Más información: VS Code - Nodemon Herramientas de desarrollo 17 https://github.com/Microsoft/vscode-recipes/tree/master/nodemon Creando un simple Web Server Antes de comenzar el proyecto vamos a crear un servidor Web sencillo, ya que nuestra aplicación será una REST API Web que se podrá consumir mediante el protocolo HTTP, será un API para administrar un listado de tareas de cada usuario y que opcionalmente se pueden organizar en grupos, todo lo anterior con persistencia de datos. Tomaremos el ejemplo del Web Server que se encuentra en la documentación de Node.js y reemplazamos el contenido por el siguiente código: // index.js const http = require('http'); const hostname = '127.0.0.1'; const port = 3000; const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello World'); }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); }); En el ejemplo anterior Node.js utiliza el módulo de http para crear un servidor Web que escuchará todas las peticiones en la dirección IP y puerto establecidos, como es un ejemplo responderá a cualquier solicitud con la cadena Hello World en texto plano sin formato, solo para mostrar que el servidor Web está funcionando accedemos al navegador en la siguiente dirección: http://localhost:3000/ Normalmente en un REST API Web se consume mediante el protocolo HTTP y la respuesta son objetos en formato JSON (el cual introduciremos más adelante), organizado en una serie de rutas que adicionalmente reciben parámetros, el contrato para establecer la relación entre las acciones del REST API Web y los verbos HTTP es bastante estandarizada, pero no la estructura de la respuesta y la organización de la rutas, en esta aplicación comenzaremos con una estructura sugerida que irá cambiando dependiendo los diferentes casos y necesidades que se presenten. Crear un simple Web Server 18 https://nodejs.org/api/synopsis.html Antes de finalizar hacemos commit de nuestro trabajo: git add index.js git commit -m "Create a simple Web server" Más información: HTTP Crear un simple Web Server 19 https://nodejs.org/dist/latest-v6.x/docs/api/http.html Utilizando Express JS Express JS es un librería con un alto nivel de popularidad en la comunidad de Node.js e inclusive esta mismo adoptó Express JS dentro de su ecosistema, lo cual le brinda un soporte adicional, pero esto no quiere decir que sea la única o la mejor, aunque existen muchas otras librerías y frameworks para crear REST API, Express JS si es la más sencilla y poderosa, según la definición en la página Web de Express JS: "Con miles de métodos de programa de utilidad HTTP y middleware a su disposición, la creación de una API sólida es rápida y sencilla." Añadiendo Express JS Instalamos el módulo de express como una dependencia de nuestra aplicación: npm install -S express Esto añadirá una nueva entrada en nuestro archivo package.json en la sección de dependencies indicando la versión instalada. Creamos un directorio llamado server donde se almacenarán todos los archivos relacionados con el servidor Web así como su nombre lo indica: mkdir server touch server/index.js Tomaremos como ventaja la convención de Node.js en la nomenclatura de los módulos de la raíz de cada carpeta llamándolos index.js Tomaremos el ejemplo de Hello World que se encuentra en la documentación de Express JS y reemplazamos el contenido del archivo por el siguiente código: Utilizando Express JS 20 https://nodejs.org/en/blog/announcements/foundation-express-news/ http://expressjs.com/ https://expressjs.com/en/starter/hello-world.html // server/index.js const express = require('express'); const port = 3000; const app = express(); app.get('/', (req, res) => { res.send('Hello World!'); }); app.listen(port, () => { console.log(`Example app listening on port ${port}!`); }); Como lo vemos es muy similar al anterior pero con una sutiles diferencias: Utilizamos la librería de express en vez del módulo http . Creamos una aplicación de express (inclusive se pueden crear varias) y la almacenamos en la variable app . Esta vez solo aceptaremos peticiones en la raíz del proyecto con el verbo GET de HTTP. Si el servidor se está ejecutando lo detenemos con CTRL+C , lo ejecutamos nuevamente con el comando node server/index y abrimos el navegador en la siguiente dirección http://localhost:3000/ . Luego volvemos a ejecutar la aplicación en modo de desarrollo. Aparentemente vemos el mismo resultado, pero express nos ha brindado un poco más de simplicidad en el código y ha incorporado una gran cantidad de funcionalidades, las cuales veremos más adelante. Organizando la aplicación Antes de continuar vamos a organizar nuestra aplicación, crearemos diferentes módulos cada uno con su propósito específico, ya que esto permite separar la responsabilidad de cada uno de ellos, organizarlos en directorios para agrupar los diferentes módulos con funcionalidad común, así nuestro archivo index.js será más ligero, legible y estructurado, ya que NO es una buena práctica colocar todo el código en un solo archivo que sería difícil de mantener, esto se le conoce como aplicación monolítica. Empezamos organizando la aplicación de Express JS: Utilizando Express JS 21 // server/index.js const express = require('express'); const app = express(); app.get('/', (req, res) => { res.send('Hello World'); }); module.exports = app; Como podemos observar ahora nuestra aplicación de express es un módulo que tiene una sola responsabilidad de crear nuestra aplicación y es independiente a la librería que utilizamos. Creamos el directorio y módulo básico de configuración: mkdir -p server/config touch server/config/index.js En el archivo de configuración es donde centralizamos todas las configuraciones de la aplicación que hasta el momento tenemos, así cualquier cambio será en un solo archivo y no en varios, finalmente lo exportamos como un módulo: // server/config/index.js const config = { server: { port: 3000, }, }; module.exports = config; Para esta aplicación el hostname no es obligatorio, por lo cual lo omitimos en la configuración. De vuelta en nuestro archivo raíz, lo reescribimos quitándole las múltiples responsabilidades que tenía y dejándolo solo con la responsabilidad de iniciar cada uno de los componentes de la aplicación, utilizando los módulos creados posteriormente: Utilizando Express JS 22 // index.js const http = require('http'); const app = require('./server'); const config = require('./server/config'); const { port } = config.server; const server = http.createServer(app); server.listen(port, () => { console.log(`Server running at port: ${port}`); }); Como podemos observar la librería http es compatible con la versión de servidor nuestra aplicación ( app ) creada por Express JS, que se crea en la función createServer e inclusive si en el futuro fuera a incorporar un servidor https , solo es incluir la librería y duplicar la línea de creación del servidor. Reiniciamos nuestro servidor e iniciamos en modo desarrollo: npm run dev Si visitamos nuevamente el navegador en la dirección http://localhost:3000 todo debe estar funcionando sinningún problema. Antes de finalizar hacemos commit de nuestro trabajo git add --all git commit -m "Add Express JS and configuration file" Más información: Express JS Utilizando Express JS 23 http://expressjs.com/ Configuración y variables de entorno Cross-env Nuestra aplicación no siempre se ejecutará en ambiente desarrollo, por lo cual debemos establecer cuál es el ambiente en que se está ejecutando y dependiendo de ello podemos utilizar diferentes tipos de configuración e incluso conectarnos a diferentes fuentes de datos, para ello vamos a utilizar la variable de entorno process.NODE_ENV, que puede ser accedida desde cualquier parte de nuestra aplicación por el objeto global process . Es muy importante garantizar la compatibilidad entre los diferentes sistemas operativos ya que cada sistema establece las variables de manera diferente, para asegurar que funcione en diferentes sistemas operativos utilizaremos una librería de npm llamada cross-env , la instalamos como una dependencia de nuestra proyecto y así aseguramos que siempre esté disponible en nuestra aplicación: npm i -S cross-env Entonces finalmente para utilizarla modificaremos nuestros npm scripts en el archivo package.json de la siguiente manera: "scripts": { "dev": "cross-env NODE_ENV=development nodemon --inspect", "start": "cross-env NODE_ENV=production node index", "test": "cross-env NODE_ENV=test echo \"Error: no test specified\"&& exit 1" }, Antes de finalizar hacemos commit de nuestro trabajo: git add --all git commit -m "Add cross-env and setting the environments" Más información acerca de: process cross-env Dotenv Configuración y variables de entorno 24 https://nodejs.org/api/process.html https://www.npmjs.com/package/cross-env En este momento nuestras variables de configuración están almacenadas en un archivo JavaScript y sus valores están marcados en el código fuente de la aplicación, lo cual NO es una buena práctica pues se estaría exponiendo datos sensibles, la idea es extraer esta información y tenerla en archivos separados por entorno pues no es la misma base de datos de desarrollo que la de producción. Ahora debemos distinguir que es configuración de sistema y del usuario, la primera son valores que necesita la aplicación para funcionar como: numero del puerto, la dirección IP y demás, las segundas son más relacionadas con la personalización de la aplicación como: número de objetos por página, personalización de la respuesta al usuario, etc. Utilizaremos una librería que nos permite hacer esto muy fácil llamada dotenv, que nos carga todas las variables especificadas en un archivo a el entorno de Node JS, procedemos a crear el archivo de configuración en la raíz del proyecto llamado .env y le colocamos el siguiente contenido: SERVER_PORT=3000 Instalamos la librería: npm i -S dotenv Procedemos a modificar nuestro archivo de configuración: // server/config/index.js require('dotenv').config(''); const config = { server: { port: process.env.SERVER_PORT || 3000, }, }; module.exports = config; Como podemos ver por defecto la librería dotenv buscará el archivo llamado .env , cargará las variables y su valores en el entorno del proceso donde se está ejecutando Node JS. Así nuestra configuración del sistema no estará más en el código fuente de la aplicación y adicional podemos reemplazar el archivo de configuración en cada entorno donde se publique la aplicación. Configuración y variables de entorno 25 Antes de terminar un aspecto muy importante es NO incluir el archivo de configuración .env en el repositorio de git, pues esto es justo lo que queremos evitar: que nuestra configuración y valores no quede en el código fuente de la aplicación, para ello modificamos el archivo .gitignore de la siguiente forma: node_modules/ .DS_Store .Thumbs.db .env Como no estamos guardando este archivo en nuestro repositorio, crearemos una copia de ejemplo para que el usuario pueda renombrarlo y utilizarlo: cp .env .env.example Antes de finalizar hacemos commit de nuestro trabajo: git add --all git commit -m "Add dotenv, .env file and set in the configuration file" Más información acerca de: dotenv Configuración y variables de entorno 26 https://github.com/motdotla/dotenv Middleware para manejo de errores Hasta el momento la única ruta que tenemos configurada en nuestro proyecto con express es / (La raíz del proyecto) y con el método GET, pero ¿Qué sucede si tratamos de acceder otra ruta?. Pues en este momento no tenemos configurado como manejar esta excepción, en términos de express no existe ningún "Middleware" que la procese, por lo tanto terminará en un error no manejado por el usuario. Si vamos al navegador y tratamos de acceder a una ruta como localhost:3000/posts obtendremos un resultado generico cómo: Cannot GET /posts y un código HTTP 404 que indica que el recurso no fue encontrado, pero nosotros podemos controlar esto con express, primero analizemos el fragmento de código que define la ruta: app.get('/', (req, res) => { res.send('Hello World'); }); 1. La aplicación ( app ) adjunta una función callback para cuando se realice una petición a la ruta / con el método GET. 2. Esta función callback recibe dos argumentos: el objeto de la petición (request) abreviado req y el objeto de la respuesta (response) abreviado res . 3. Una vez se cumplen los criterios establecidos en la definición de la ruta la función de callback es ejecutada. 4. Se envía la respuesta por parte del servidor ( res.send('Hello world') ) el flujo de esta petición se detiene y el servidor queda escuchando por más peticiones. Express middleware "Las funciones de middleware son funciones que tienen acceso al objeto de solicitud (req), al objeto de respuesta (res) y a la siguiente función de middleware en el ciclo de solicitud/respuestas de la aplicación. La siguiente función de middleware se denota normalmente con una variable denominada next." (Tomado de Utilización de middleware) Esto quiere decir que nuestro callback en realidad es una función middleware de express y tiene la siguiente firma de parámetros: Middleware para manejo de errores 27 http://expressjs.com/es/guide/using-middleware.html app.get('/', (req, res, next) => { res.send('Hello World'); }); Dónde next es otra función que al invocarse permite continuar el flujo que habíamos mencionado antes para que otras funciones middleware pueden continuar con el procesamiento de la petición. Esto quiere decir que podemos agregar otro middleware al final del flujo para capturar si la ruta no fue procesada por ningún middleware definido anteriormente: // server/index.js const express = require('express'); const app = express(); app.get('/', (req, res, next) => { res.send('Hello World'); }); // No route found handler app.use((req, res, next) => { res.status(404); res.json({ message: 'Error. Route not found', }); }); module.exports = app; Nótese varias cosas importantes en el fragmento de código anterior: 1. Se ha agregado la función next al middleware de la ruta raíz, pero realmente no lo estamos utilizando dentro de la función, pero esto es con el fin hacer concordancia con la firma que tienen las funciones middleware (Más adelante la editaremos) 2. Le hemos indicado a la aplicación ( app ) utilizar una función middleware, nuevamente podemos saber que es una función middleware de express por su firma, revisando los parámetros. 3. Estamos estableciendo el código HTTP de la respuesta con res.status(404) el cual equivalente a una página no encontrada, en este caso una ruta o recurso. 4. Cuando un servidor Web responde debe indicar el tipo de contenido que está devolviendo al cliente (por ejemplo: Navegador Web) para que esté a su vez puede interpretarlo y mostrarlo al usuario, esto se indica en la respuesta mediante el encabezado Content-type , por defecto el valor es text/plain que se utiliza para texto Middleware para manejo de errores 28 plano o text/html para códigoHTML, cada uno de estos valores se conoce como el MIME Type de los archivos. En este caso estamos respondiendo explícitamente un objeto tipo JSON como lo indicamos en la función res.json() , no tenemos necesidad de establecer el Content-Type , pues express lo hace automáticamente por nosotros a application/json y adicionalmente convierte el objeto literal de JavaScript a un objeto JSON como tal. Guardamos el archivo (nodemon reiniciará nuestra aplicación automáticamente ) y ahora si vamos nuevamente al navegador e invocamos nuevamente la ruta localhost:3000/posts obtendremos el mensaje que establecimos en el middleware que se convierte en la última función de middleware de todos las declaradas anteriormente en esta aplicación de express: { message: 'Error. Route not found' } Para hacer una analogía a la definición de los middleware es como una cola de funciones donde la petición se revisa desde comenzando desde la primera función, si el middleware coincide con la petición este detiene el flujo, procesa la petición y puede hacer dos cosas: dar respuesta a la petición y en este caso se detiene la revisión por completo o dejar pasar la petición al siguiente middleware esto se hace con la función next, o de lo contrario si ninguna coincide llegará al final que es el middleware genérico para capturar todas las peticiones que no se pudieron procesar antes. Manejo de errores Ahora surge la siguiente pregunta, ¿Cómo podemos controlar los errores que sucedan en las diferentes rutas?, express permite definir una función middleware con una firma de parámetros un poco diferente a las anteriores, introducimos el siguiente código al final: Middleware para manejo de errores 29 // server/index.js ... // Error handler app.use((err, req, res, next) => { const { statusCode = 500, message, } = err; res.status(statusCode); res.json({ message, }); }); module.exports = app; Los ... en el código indica que hay código anterior en el archivo Podemos ver varias cosas en el código anterior: 1. El middleware que captura errores en express comienza por el parámetro de error abreviado como err 2. Obtenemos el statusCode del objeto err , si no trae ninguno lo establecemos en 500 por defecto que significa Internal Server Error, así como el message que este trae. 3. Finalmente establecemos el StatusCode en la respuesta ( res ) y enviamos de vuelta un JSON con el mensaje del error. Ahora cada vez que invoquemos un error en cualquier middleware tendremos uno que lo capture y procese, podríamos aprovechar esto, por ejemplo, para guardar en los logs información relevante del error. Para terminar cambiamos nuevamente la definición de la ruta raíz de la siguiente manera: ... app.get('/', (req, res, next) => { res.json({ message: 'Welcome to the API', }); }); ... Middleware para manejo de errores 30 Nótese que ahora estamos devolviendo un archivo JSON, pues estamos creando un REST API Web. Antes de continuar guardemos nuestro progreso: git add --all git commit -m "Add middleware for not found routes and errors" Middleware para manejo de errores 31 Sistema de Logs Utilizar Winston para los logs Es importante para cualquier aplicación que va a salir a producción tener un sistema de logs, pues cuando estamos en desarrollo tenemos el control total y es muy fácil investigar para averiguar si existe algún error, lo que no sucede cuando la aplicación está ejecutándose en producción, por lo tanto el sistema de logs es una manera eficaz de dejar rastro de las operaciones, advertencias o errores que puedan suceder en la aplicación. Existen muchas librerías que son de mucha utilidad así como express, entre ellos está Winston, el cual nos permite crear un log personalizado con múltiples métodos para manejar los logs desde imprimirlos en la consola hasta guardarlos en archivos. Procedemos a instalar las dependencias npm install -S winston Debemos crear un nuevo archivo para establecer la configuración de los logs, estoy es muy importante si en el día de mañana se quiere cambiar de configuración o inclusive de librería el resto de la aplicación no debería verse afectada. Creamos el archivo con su respectivo contenido: // server/config/logger.js const { createLogger, format, transports } = require('winston'); // Setup logger const logger = createLogger({ format: format.simple(), transports: [new transports.Console()], }); module.exports = logger; Como pueden observar hemos decidido para este proyecto imprimir los logs en la consola, pero como mencionamos antes esta librería nos permite almacenarlos en archivos, si así lo deseamos. Sistema de Logs 32 // server/index.js const express = require('express'); const logger = require('./config/logger'); // Init app const app = express(); // Routes app.get('/', (req, res, next) => { res.json({ message: 'Welcome to the API', }); }); // No route found handler app.use((req, res, next) => { const message = 'Route not found'; const statusCode = 404; logger.warn(message); res.status(statusCode); res.json({ message, }); }); // Error handler app.use((err, req, res, next) => { const { statusCode = 500, message } = err; logger.error(message); res.status(statusCode); res.json({ message, }); }); module.exports = app; Como podemos observar en el fragmento de código anterior incluimos nuestro logger y lo utilizamos en los middleware. Antes de continuar guardemos nuestro progreso: git add --all git commit -m "Add Winston to create log system" Sistema de Logs 33 Más información: Winston Utilizar morgan para hacer logs de las peticiones La librería anterior nos permitió crear un log personalizado, pero también debemos a registrar el acceso a todas las peticiones dándonos un detalle de lo que se está recibiendo en el servidor, para esto vamos a utilizar la librería de morgan, procedemos a instalarla de la siguiente manera: npm install -S morgan Morgan finalmente es un middleware que tomará las peticiones y las imprimirá según nosotros le indiquemos: // server/index.js const express = require('express'); const morgan = require('morgan'); const logger = require('./config/logger'); // Init app const app = express(); // Setup middleware app.use(morgan('combined')); // Routes app.get('/', (req, res, next) => { res.json({ message: 'Welcome to the API', }); }); ... Incluimos morgan como un middleware, probamos accediendo a diferentes URL y vemos el resultado en la consola, pero tenemos dos salidas de logs: una producida por winston y otra por morgan, que casualmente ambas están saliendo a la consola por lo tanto añadimos la siguiente configuración adicional: Sistema de Logs 34 https://www.npmjs.com/package/winston // server/index.js ... // Setup middleware app.use(morgan('combined', { stream: { write: message => logger.info(message) } })); ... Resulta y pasa que por defecto morgan coloca sus logs en la consola, pero nosotros le estamos indicando que utilice el logger que creamos con winston, finalmente irán a la consola, pero esta vez es un solo manejador de logs, que si decidimos guardarlos en archivos, nuevamente el cambio sería solamente en una sola parte. Si abrimos el navegador e invocamos diferentes rutas en la dirección del navegador Web podremos ver en la consola las diferentes peticiones que llegan al servidor y también los mensajes personalizados que imprimimos en los middleware. Antes de continuar guardemos nuestro progreso: git add --all git commit -m "Add morgan to log the app requests" Más información: Morgan Hacer seguimiento a las peticiones Nuevamente cuando nuestra aplicación está ejecutándose en producción llegan muchísimas peticiones al mismo tiempo y si deseamos identificar una en particular va a hacer muy difícil, para ello debemos identificar cada uno de ellas, utilizaremos una librería para ello llamada express-request-id que añade a la petición entrante (req) un campo llamado id donde su valor es un número único yen la petición de respuesta (res) establece el encabezado X-Request-Id con el mismo valor para que el cliente pueda identificar la petición. procedemos a instalarla de la siguiente manera: npm install -S express-request-id La incluimos en nuestro archivo del servidor: Sistema de Logs 35 https://www.npmjs.com/package/morgan // server/index.js const express = require('express'); const morgan = require('morgan'); const requestId = require('express-request-id')(); const logger = require('./config/logger'); // Init app const app = express(); // Setup middleware app.use(requestId); app.use(morgan('combined', { stream: { write: message => logger.info(message) } })); ... Al redirigir los mensajes de morgan a winston las peticiones siempre agregan un final de línea, por lo tanto vamos a removerlo con la siguiente librería: npm i -S strip-final-newline Ahora debemos incluirla en nuestro log de morgan, por lo tanto vamos a mover morgan a nuestro archivo de logger y lo editamos de la siguiente manera: Sistema de Logs 36 // server/config/logger.js const { createLogger, format, transports } = require('winston'); const morgan = require('morgan'); const stripFinalNewline = require('strip-final-newline'); // Setup logger const logger = createLogger({ format: format.simple(), transports: [new transports.Console()], }); // Setup requests logger morgan.token('id', req => req.id); const requestFormat = ':remote-addr [:date[iso]] :id ":method :url" :status'; const requests = morgan(requestFormat, { stream: { write: (message) => { // Remove all line breaks const log = stripFinalNewline(message); return logger.info(log); }, }, }); // Attach to logger object logger.requests = requests; module.exports = logger; Explicamos a continuación los cambios: Le indicamos a morgan como va a calcular el token :id , que como vimos anteriormente nuestra nueva librería lo agrega al objeto req . Establecemos un formato personalizado basado en el combined que trae por defecto y añadimos el token :id . Eliminamos todos los saltos de línea y guardamos toda la configuración de morgan en la variable requests . Finalmente guardamos dentro de logger la configuración de morgan, pues recordemos que debemos añadirla como middleware de express en el archivo del servidor. Actualizamos nuevamente nuestro archivo del servidor: Sistema de Logs 37 // server/index.js const express = require('express'); const requestId = require('express-request-id')(); const logger = require('./config/logger'); // Init app const app = express(); // Setup middleware app.use(requestId); app.use(logger.requests); ... Antes de continuar guardemos nuestro progreso: git add --all git commit -m "Add Request id to identify the request with morgan" Más información: express-request-id Añadir formato de la petición a los logs En este momento los logs generados por morgan al recibir las peticiones tienen una información adicional de la petición como: Dirección IP del cliente, fecha y hora, el método HTTP y la URL, añadimos un función que reciba la petición como parámetro y nos devuelva la misma información: // server/config/logger.js ... // Attach to logger object logger.requests = requests; // Format as request logger and attach to logger object logger.header = (req) => { const date = new Date().toISOString(); return `${req.ip} [${date}] ${req.id} "${req.method} ${req.originalUrl}"`; }; module.exports = logger; Sistema de Logs 38 https://github.com/floatdrop/express-request-id A continuación vamos a realizar las siguientes modificaciones en nuestro archivo del servidor en los dos middleware asociados a errores: // server/index.js ... // No route found handler app.use((req, res, next) => { next({ message: 'Route not found', statusCode: 404, level: 'warn', }); }); // Error handler app.use((err, req, res, next) => { const { message, statusCode = 500, level = 'error' } = err; const log = `${logger.header(req)} ${statusCode} ${message}`; logger[level](log); res.status(statusCode); res.json({ message, }); }); module.exports = app; Lo primero que podemos observar es que ahora el middleware que nos captura cuando una ruta no es encontrada está invocando el siguiente middleware ( next ) con el información del error y finalmente en procesado por nuestro middleware asociado con los errores, donde creamos la cadena de log basada en la función que acabamos de crear, una nueva modificación es el type que por defecto es error si no es enviada en el objeto de error . Si vamos al navegador e invocamos una dirección que no existe sobre nuestra API podemos ver el resultado en los logs. Antes de continuar guardemos nuestro progreso git add --all git commit -m "Add log header from requests" Sistema de Logs 39 Introducción ¿Que es un API? Inicializando el proyecto Herramientas de desarrollo Crear un simple Web Server Utilizando Express JS Configuración y variables de entorno Middleware para manejo de errores Sistema de Logs Postman Utilizando el Router y Routes de Express Creando el layout del API Capturando y procesando parámetros de las peticiones Patrones asincrónicos de JavaScript Callbacks Promises async / await Bases de datos SQL y NoSQL Instalando y configurando MongoDB Conectando con MongoDB Mongoose Models Procesando parámetros comunes con middleware Estandarización de la respuesta Mongoose Schemas Paginación Ordenamiento Creando recursos Relaciones entre recursos Consultar recursos anidados Añadir recursos anidados Añadir y remover campos de un documento Administración de usuarios Autenticación de usuarios Autorización de usuarios Validaciones personalizadas Limpieza de campos Restricción de las peticiones Pruebas Configurando pruebas Pruebas unitarias Pruebas de integración Configurando Swagger y JSDoc Documentando las rutas y creando modelos Conclusión
Compartir