Creando Apis Con Node Js Express y Mongodb
Creando Apis Con Node Js Express y Mongodb
Portada
Introducción 1.1
¿Que es un API? 1.2
Persistencia de datos
2
Bases de datos SQL y NoSQL 5.1
Instalando y configurando MongoDB 5.2
Conectando con MongoDB 5.3
Mongoose Models 5.4
Procesando parámetros comunes con middleware 5.5
Estandarización de la respuesta 5.6
Mongoose Schemas 5.7
Paginación 5.8
Ordenamiento 5.9
Creando recursos 5.10
Relaciones entre recursos 5.11
Consultar recursos anidados 5.12
Añadir recursos anidados 5.13
Asegurando el API
Añadir y remover campos de un documento 6.1
Administración de usuarios 6.2
Autenticación de usuarios 6.3
Autorización de usuarios 6.4
Validaciones personalizadas 6.5
Limpieza de campos 6.6
Restricción de las peticiones 6.7
Probando el API
Pruebas 7.1
Configurando pruebas 7.2
Pruebas unitarias 7.3
Pruebas de integración 7.4
Documentando el API
Configurando Swagger y JSDoc 8.1
3
Documentando las rutas y creando modelos 8.2
Conclusión 8.3
4
Introducción
Introducción
Arrow functions
let + const
Template Strings
Destructuring assignment
Promises
Si eres nuevo en Node.js se recomienda leer el siguiente libro de este misma serie:
Introducción a Node JS
5
Introducción
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.
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.
6
¿Que es un API?
¿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.
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.
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.
7
¿Que es un API?
Status
Verbo Operación Descripción
Code
/api/tasks/ POST 201 CREATE Crear un tarea
8
¿Que es un API?
Este contrato 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.
9
Inicializando el proyecto
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.
git init
touch .gitignore
node_modules/
.DS_Store
Thumbs.db
touch index.js
"scripts": {
"start": "node index",
"test": "echo \"Error: no test specified\" && exit 1"
},
10
Inicializando el proyecto
11
Herramientas de desarrollo
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.
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"
},
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 .
Más información:
12
Herramientas de desarrollo
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:
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.
./node_modules/.bin/eslint --init
Alternativamente con la utilidad npx , instalada globalmente por Node.js, con el siguiente
comando:
13
Herramientas de desarrollo
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:
14
Herramientas de desarrollo
{
"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"
}
]
}
}
Más información:
ESLint
mkdir .vscode
touch .vscode/settings.json
15
Herramientas de desarrollo
{
"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:
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
{
"recommendations": [
"waderyan.nodejs-extension-pack",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
16
Herramientas de desarrollo
touch .vscode/launch.json
{
"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.
Más información:
VS Code - Nodemon
17
Crear un simple Web Server
// index.js
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/
18
Crear un simple Web Server
Más información:
HTTP
19
Utilizando Express JS
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:
mkdir server
touch server/index.js
20
Utilizando Express JS
// server/index.js
app.listen(port, () => {
console.log(`Example app listening on port ${port}!`);
});
Como lo vemos es muy similar al anterior pero con una sutiles diferencias:
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.
21
Utilizando Express JS
// server/index.js
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.
mkdir -p server/config
touch server/config/index.js
// server/config/index.js
const config = {
server: {
port: 3000,
},
};
module.exports = config;
22
Utilizando Express JS
// index.js
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.
Más información:
Express JS
23
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 .
npm i -S cross-env
"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"
},
process
cross-env
Dotenv
24
Configuración y variables de entorno
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
// 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.
25
Configuración y variables de entorno
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
dotenv
26
Middleware para manejo de errores
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:
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."
Esto quiere decir que nuestro callback en realidad es una función middleware de express y
tiene la siguiente firma de parámetros:
27
Middleware para manejo de errores
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
module.exports = app;
28
Middleware para manejo de errores
plano o text/html para código HTML, 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.
{
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:
29
Middleware para manejo de errores
// 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;
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.
...
...
30
Middleware para manejo de errores
Nótese que ahora estamos devolviendo un archivo JSON, pues estamos creando un REST
API Web.
31
Sistema de Logs
Sistema de Logs
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.
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.
// server/config/logger.js
// 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.
32
Sistema de Logs
// server/index.js
// Init app
const app = express();
// Routes
app.get('/', (req, res, next) => {
res.json({
message: 'Welcome to the API',
});
});
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;
33
Sistema de Logs
Más información:
Winston
Morgan finalmente es un middleware que tomará las peticiones y las imprimirá según
nosotros le indiquemos:
// server/index.js
// Init app
const app = express();
// Setup middleware
app.use(morgan('combined'));
// Routes
app.get('/', (req, res, next) => {
res.json({
message: 'Welcome to the API',
});
});
...
34
Sistema de Logs
// 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.
Más información:
Morgan
35
Sistema de Logs
// server/index.js
// 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:
36
Sistema de Logs
// server/config/logger.js
// Setup logger
const logger = createLogger({
format: format.simple(),
transports: [new transports.Console()],
});
module.exports = logger;
37
Sistema de Logs
// server/index.js
// Init app
const app = express();
// Setup middleware
app.use(requestId);
app.use(logger.requests);
...
Más información:
express-request-id
// server/config/logger.js
...
module.exports = logger;
38
Sistema de Logs
// server/index.js
...
// 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.
39