En este tutorial vamos a crear una API con Node.js. A modo de ejemplo y para no caer en el aburrimiento, crearemos la API de una aplicación que nos permita controlar los gastos de nuestros viajes.
No crearemos el frontend, ya que en este tutorial nos centraremos en la API y en el uso de Express. Además de Express, también necesitaremos una base de datos para almacenar los datos de forma persistente. Para ello usaremos MongoDB. Podrás encontrar el código completo del proyecto que vamos a crear en este repositorio de GitHub.
Contenidos
Introducción
Vamos a crear una aplicación que nos permita llevar el control de los gastos de nuestros viajes. Para seguir este tutorial necesitarás ciertos conocimientos previos básicos. Por ejemplo, necesitas tener unas nociones básicas acerca de lo que es una API y, en caso de que más adelante vayas a usar la API con una aplicación, también necesitarás saber cómo conectarte a la API mediante JavaScript. Del mismo modo, también es recomendable que tengas ciertas nociones básicas de JavaScript y de Node.js:
- Aprende JavaScript: Guía definitiva de JavaScript
- Tutorial de Node.Js: Tutorial de introducción a Node.js
Además, también necesitarás ciertos conocimientos básicos acerca de la línea de comandos. Si nunca la has utilizado, también puedes consultar el siguiente tutorial:
- Cómo usar la línea de comandos: Tutorial de introducción a la línea de comandos
Necesitamos gestionar viajes, por lo que nuestra API debe soportar la creación de viajes, ya que de lo contrario malamente podremos controlar los gastos de algo que no existe. También necesitaremos otro endpoint para obtener los viajes creados. Con respecto a los gastos, necesitamos como mínimo un endpoint que nos permita crear gastos y otro que nos permita obtenerlos. Para probar la API puedes usar herramientas como Postman o Insomnia.
Estos serán los endpoints que tendrá nuestra api:
- POST
/viajes
: Esta ruta nos permitirá enviar el nombre del viaje que vamos a crear. - GET
/viajes
: Mediante esta ruta podremos obtener los viajes existentes. - POST
/viajes/:viajeId/gastos
: Esta ruta nos permitirá agregar un gasto a un viaje existente, aceptado el nombre del mismo, su fecha de creación, su categoría y su descripción. Indicaremos el viaje mediante el parámetroviajeId
de la ruta. - GET
/viajes/viajeId/gastos
: Mediante esta ruta podremos obtener los gastos asociados a un viaje. De nuevo, filtraremos el viaje usando el parámetroviajeId
de la ruta.
Tal y como ves, también haremos uso de categorías. Podríamos gestionarlas también desde la API, aunque en este tutorial serán categorías fijas usadas a modo de etiquetas.
Inicialización del proyecto
En este apartado vamos a ver cómo instalar los paquetes que necesitamos, además de ver cómo crear el servidor y las rutas que necesitamos.
Instalación de paquetes
Lo primero que tendrás que hacer es instalar Node.js en tu sistema en caso de que no lo tengas instalado. Para ello, consulta el tutorial de instalación de Node.js, en donde verás los pasos que debes seguir para instalar Node.js en Windows, Linux y macOS.
Una vez tengas Node.js instalado, abre una ventana de línea de comandos y crea un nuevo directorio en el lugar en donde quieras crear el proyecto mediante el comando mkdir
:
mkdir viajes
Luego accede al directorio usando el comando cd
:
cd viajes
Seguidamente, inicializa el proyecto mediante el siguiente comando:
npm init
Se te harán una serie de preguntas. Presione ENTER
para aceptar los valores por defecto.
Ahora tendrás que instalar los paquetes necesarios. Para comenzar, instala Express usando el siguiente comando:
npm install express
Dado que usaremos MongoDB para nuestra base de datos, también tendrás que instalar el paquete de MongoDB para Node.js. Para ello ejecuta el siguiente comando:
npm install mongodb
Finalmente, también tendrás que instalar el Body Parser, que es un paquete que te permitirá obtener los datos del cuerpo de las peticiones HTTP:
npm install body-parser
Tras esto, ya habremos instalado todos los paquetes necesarios.
Creación del servidor
Vamos a crear e iniciar el servidor, mediante el cual nos podremos comunicar con la API. Para ello, crea el archivo server.js
y edítalo con tu editor preferido.
En la pare superior del archivo, usa la sentencia require
para incluir tanto Express como el Body Parser como MongoDB en el proyecto:
const express = require('express');
const bodyParser = require('body-parser');
const mongo = require('mongodb').MongoClient;
const ObjectId = require('mongodb').ObjectId;
Express es el framework de Node.js que se usa para crear aplicaciones web. El body parser es una librería que se usa para leer el cuerpo o body de las peticiones entrantes. Antes se incluía por defecto junto con Express, aunque hace ya varias versiones que ha dejado de ser así.
Si te fijas, también hemos agregado la clase ObjectId, que nos permitirá comprobar si una cadena de texto es un identificador válido de un registro de Mongo, además de permitirnos obtener un identificador usando una cadena de texto.
Luego debemos iniciar Express y crear una instancia del body parser en la constante jsonParser
:
const app = express();
const jsonParser = bodyParser.json();
Hemos usado el parser de JSON, ya que en este proyecto enviaremos las peticiones con JSON en el cuerpo de las mismas.
Ahora debemos agregar las rutas que necesitamos, aunque por ahora no realizarán ninguna función:
app.post('/viajes', (req, res) => { /* Código */ });
app.get('/viajes', (req, res) => { /* Código */ });
app.post('/viajes/:viajeId/gastos', (req, res) => { /* Código */ });
app.get('/viajes/:viajeId/gastos', (req, res) => { /* Código */ });
Finalmente, vamos a indicar a nuestra aplicación que escuche las peticiones al puerto 3000 usando el método listen()
de la aplicación app
:
app.listen(3000, () => console.log('Servidor iniciado'));
Ahora regresa a la línea de comandos e inicia la aplicación usando el siguiente comando:
node server.js
Ahora nuestro servidor ya será capaz de atender peticiones en el puerto 3000.
Conexión con MongoDB
Ya hemos incluido la librería de MongoDB en nuestro proyecto, por lo que ya podemos usarla en nuestra implementación. Sin embargo, debes asegurarte de que MongoDB está instalado en tu sistema. Si no lo está, dirígete a la página de descargas de MongoDB y descárgate el instalador para tu sistema operativo.
Junto con MongoDB también se instalará Compass, que es una herramienta que te permitirá gestionar las bases de datos MongoDB. Cuando finalice la instalación, inicia Compass con la conexión por defecto a tu sistema local, que es mongodb://localhost:27017
.
Luego crea la base de datos viajes
junto con las colecciones viajes
y gastos
, que por ahora no contendrán datos. No pasa nada si no las creas, ya que MongoDB las creará automáticamente la ser referenciadas.
Ahora regresa al código del archivo sever.js
. Ya hemos incluido la librería de MongoDB en nuestro proyecto mediante la siguiente línea.
const mongo = require('mongodb').MongoClient;
Sin embargo todavía tenemos que conectarnos a nuestra base de datos MongoDB, que por defecto se encontrará en el puerto 27017
. Vamos a conectarnos a la base de datos mediante la función connect
:
const client = new mongo('mongodb://localhost:27017');
Si usas Windows y quieres evitarte problemas, usa esta conexión en su lugar:
const client = new mongo('mongodb://127.0.0.1:27017');
Ahora vamos a seleccionar las colecciones viajes
y gastos
que hemos creado, de modo que podamos trabajar con ellas:
client.connect();
const coleccionViajes = client.db('viajes').collection('viajes');
const coleccionGastos = client.db('viajes').collection('gastos');
Con esto ya hemos configurado la base de datos y las colecciones. Este es el código completo del archivo server.js
por ahora:
const express = require('express');
const bodyParser = require('body-parser');
const mongo = require('mongodb').MongoClient;
const ObjectId = require('mongodb').ObjectId;
const client = new mongo('mongodb://127.0.0.1:27017');
client.connect();
const coleccionViajes = client.db('viajes').collection('viajes');
const coleccionGastos = client.db('viajes').collection('gastos');
const app = express();
const jsonParser = bodyParser.json();
app.post('/viajes', (req, res) => { /* Código */ });
app.get('/viajes', (req, res) => { /* Código */ });
app.post('/viajes/:viajeId/gastos', (req, res) => { /* Código */ });
app.get('/viajes/:viajeId/gastos', (req, res) => { /* Código */ });
app.listen(3000, () => console.log('Servidor iniciado'));
Implementación del proyecto
A continuación veremos cómo implementar cada una de las rutas que necesitamos.
Agregando viajes
Vamos a comenzar implementando la ruta que nos permita crear nuevos viajes, que será la ruta POST /viajes
, que por ahora tiene el siguiente código:
app.post('/viajes', (req, res) => { /* Código */ });
Sin embargo, antes de nada, vamos a crear la función asíncrona que nos permita insertar un nuevo viaje en nuestra colección viajes. A esta función le daremos el nombre insertarViaje
y aceptará como argumentos la colección de viajes coleccionViajes
, además de le petición y la respuesta de Express:
async function insertarViaje(coleccionViajes, req, res)
{
try {
if (!req.body.hasOwnProperty('nombre')) {
throw 'No se ha definido un nombre';
}
await coleccionViajes.insertOne({ nombre: req.body.nombre });
return res.status(201).json({ success: true });
} catch (error) {
return res.status(400).json({ success: false, error});
}
}
Lo primero que hemos hecho ha sido validar los datos de entrada. Por ejemplo, en el cuerpo de la petición se debe incluir el nombre del viaje y, de no ser así, devolvemos un error 400. Para gestionar los errores hemos usado las sentencias try
y catch
.
Si está todo en orden, pasaremos a insertar el viaje en la base da datos. Mediante le método coleccionViajes.insertOne
le decimos a MongoDB que inserte el documento con los valores indicados en el parámetro datos
.
Hemos usado la sentencia await
, ya que de esta forma esperamos a que finalice la inserción. Si no ocurre ningún error, responderemos con un código 201
, indicando que la operación ha tenido éxito. El uso de success
para indicar éxito o fracaso no es ningún estándar, sino una recomendación que me saco de la manga, basada en el modo de proporcionar una respuesta a las peticiones por parte de muchos desarrolladores.
Si ocurre algún error, entonces se ejecutará la parte correspondiente al bloque catch
, respondiendo con un código de error 400 y con el error que ha ocurrido. Un posible error también podría darse en caso de que el servicio de MongoDB no se esté ejecutando.
Ahora vamos a modificar la definición de la ruta, en donde sencillamente llamaremos a la función insertarViaje
. Además, también hemos agregado el bodyParser
a modo de middleware:
app.post('/viajes', jsonParser, (req, res) => {
return insertarViaje(coleccionViajes, req, res);
});
Obteniendo viajes
A continuación vamos a implementar o la ruta que nos permita obtener la lista de viajes, que será la ruta GET /viajes
, que por ahora tiene el siguiente código:
app.get('/viajes', (req, res) => { /* Código */ });
Al igual que antes, crearemos primero al función asíncrona que nos permita obtener los viajes de la base de datos:
async function obtenerViajes(coleccionViajes, req, res)
{
try {
const viajes = await coleccionViajes.find().toArray();
return res.status(200).json({
success: true,
viajes
});
} catch (error) {
return res.status(500).json({ success: false, error});
}
}
Mediante el método find
hemos obtenido los viajes de la colección viajes. Mediante el método toArray
transformamos el resultado en un array y lo devolvemos como resultado. De nuevo, hemos usado las sentencias try
y catch
por si se produce algún error.
Seguidamente tendremos que modificar la definición de la ruta GET /viajes
, llamando a al función obtenerViajes
en su interior:
app.get('/viajes', (req, res) => {
return obtenerViajes(coleccionViajes, req, res);
});
Agregando gastos a un viaje
Ya hemos visto cómo agregar y obtener viajes. Ahora vamos a ver cómo agregar gastos a un viaje existente. Para ello usaremos la ruta POST /viajes/:viajeId/gastos
, que por ahora contiene el siguiente código:
app.post('/viajes/:viajeId/gastos', (req, res) => { /* Código */ });
Vamos a crear la función asíncrona que nos permita realizar la validación de datos e insertar gastos asociados a un viaje. Le daremos el nombre de insertarGastoViaje
. Recibirá como argumentos las colecciones coleccionViajes
y coleccionGastos
, además de la petición y la respuesta.
Lo primero que haremos en la función insertarGastoViaje
será comprobar que el parámetro viajeId
de la ruta API es válido, correspondiéndose también con un viaje existente. Antes de comprobar si existe debemos comprobar si es un identificador de documento válido en MongoDB, para lo cual usaremos el método ObjectId.isValid
. Si no lo es, lanzamos un error:
if (!ObjectId.isValid(req.params.viajeId)) {
throw 'El viaje indicado no es válido';
}
Seguidamente debemos comprobar si dicho identificador se corresponde con un viaje existente, usando el método findOne
de la colección de viajes. De no existir, lanzaremos otro error:
const viaje = await coleccionViajes.findOne(ObjectId(req.params.viajeId));
if (!viaje) {
throw 'No se ha encontrado el viaje indicado';
}
Seguidamente tendremos que comprobar que se proporcionan todos los parámetros requeridos y que la categoría está dentro de las admitidas. Finalmente insertaremos el documento usando el método insertOne
par luego devolver una respuesta con código 201. Envolveremos toda la función en una sentencia try...catch
y lanzaremos un error 400 en caso de haber algún error.
Este es el código completo de la función insertarGastoViaje
:
async function insertarGastoViaje(coleccionViajes, coleccionGastos, req, res)
{
const categorias = ['comida', 'transporte', 'alojamiento', 'otros'];
try {
if (!ObjectId.isValid(req.params.viajeId)) {
throw 'El viaje indicado no es válido';
}
const viaje = await coleccionViajes.findOne(ObjectId(req.params.viajeId));
if (!viaje) {
throw 'No se ha encontrado el viaje indicado';
}
if (!req.body.hasOwnProperty('descripcion')) {
throw 'No se ha definido una descripción';
}
if (!req.body.hasOwnProperty('cantidad')) {
throw 'No se ha definido una cantidad';
}
if (!req.body.hasOwnProperty('categoria') || !categorias.includes(req.body.categoria)) {
throw 'No se ha definido una categoría váida';
}
if (!req.body.hasOwnProperty('fecha')) {
throw 'No se ha definido una fecha';
}
await coleccionGastos.insertOne({
viajeId: ObjectId(req.params.viajeId),
descripcion: req.body.descripcion,
cantidad: req.body.cantidad,
categoria: req.body.categoria,
fecha: req.body.fecha
});
return res.status(201).json({ success: true });
} catch (error) {
return res.status(400).json({ success: false, error});
}
}
Para terminar, vamos a modificar la definición de la ruta POST /viajes/:viajeId/gastos
, de forma que invoquemos a la función insertarGastoViaje
en su interior:
app.post('/viajes/:viajeId/gastos', jsonParser, (req, res) => {
return insertarGastoViaje(coleccionViajes, coleccionGastos, req, res);
});
Obteniendo los gastos de un viaje
Finalmente, ya solo nos falta agregar el código que nos permita obtener los gastos de un viaje. Para tal fin usaremos la ruta GET /viajes/:viajeId/gastos
, que hemos definido tal que así:
app.get('/viajes/:viajeId/gastos', (req, res) => { /* Código */ });
Vamos a crear la función asíncrona obtenerGastosViaje
. Debemos pasarle como argumentos las colecciones coleccionViajes
y coleccionGastos
. Agregaremos un bloque try...catch
y en su interior, al igual que hemos hecho en al función insertarGastoViaje
, debemos comprobar si el parámetro viajeId
de la ruta es un identificador válido y que además existe en la colección coleccionViajes
de MongoDB:
if (!ObjectId.isValid(req.params.viajeId)) throw 'El viaje indicado no es válido';;
const viaje = await coleccionViajes.findOne(ObjectId(req.params.viajeId));
if (!viaje) throw 'No se ha encontrado el viaje indicado';;
Ahora ya solo nos falta usar el método find de la colección coleccionGastos
para obtener todos los gastos del viaje definido mediante el parámetro viajeId
:
const gastos = await coleccionGastos.find({ viajeId: ObjectId(req.params.viajeId) }).toArray();
Ya solo nos falta responder a la petición con un código 200, adjuntando los gastos que hemos obtenido:
return res.status(200).json({success: true, gastos});
A continuación puedes encontrar el código completo de la función obtenerGastosViaje
:
async function obtenerGastosViaje(coleccionViajes, coleccionGastos, req, res)
{
try {
if (!ObjectId.isValid(req.params.viajeId)) throw 'El viaje indicado no es válido';;
const viaje = await coleccionViajes.findOne(ObjectId(req.params.viajeId));
if (!viaje) throw 'No se ha encontrado el viaje indicado';;
const gastos = await coleccionGastos.find({ viajeId: ObjectId(req.params.viajeId) }).toArray();
return res.status(200).json({success: true, gastos});
} catch (error) {
return res.status(400).json({ success: false, error});
}
}
Ya solo nos falta editar la definición de la ruta GET /viajes/:viajeId/gastos
, de modo que invoquemos a la función obtenerGastosViaje
en su interior:
app.get('/viajes/:viajeId/gastos', (req, res) => {
return obtenerGastosViaje(coleccionViajes, coleccionGastos, req, res);
});
Finalizando el proyecto
Con esto ya hemos terminado nuestra aplicación. A continuación puedes encontrar el código completo del archivo server.js
:
const express = require('express');
const bodyParser = require('body-parser');
const mongo = require('mongodb').MongoClient
const ObjectId = require('mongodb').ObjectId;
const client = new mongo('mongodb://127.0.0.1:27017');
client.connect();
const coleccionViajes = client.db('viajes').collection('viajes');
const coleccionGastos = client.db('viajes').collection('gastos');
const app = express();
const jsonParser = bodyParser.json();
async function insertarViaje(coleccionViajes, req, res)
{
try {
if (!req.body.hasOwnProperty('nombre')) {
throw 'No se ha definido un nombre';
}
await coleccionViajes.insertOne({ nombre: req.body.nombre });
return res.status(201).json({ success: true });
} catch (error) {
return res.status(400).json({ success: false, error});
}
}
async function obtenerViajes(coleccionViajes, req, res)
{
try {
const viajes = await coleccionViajes.find().toArray();
return res.status(200).json({
success: true,
viajes
});
} catch (error) {
return res.status(500).json({ success: false, error});
}
}
async function insertarGastoViaje(coleccionViajes, coleccionGastos, req, res)
{
const categorias = ['comida', 'transporte', 'alojamiento', 'otros'];
try {
if (!ObjectId.isValid(req.params.viajeId)) throw 'El viaje indicado no es válido';;
const viaje = await coleccionViajes.findOne(ObjectId(req.params.viajeId));
if (!viaje) throw 'No se ha encontrado el viaje indicado';
if (!req.body.hasOwnProperty('descripcion')) throw 'No se ha definido una descripción';
if (!req.body.hasOwnProperty('cantidad')) throw 'No se ha definido una cantidad';
if (!req.body.hasOwnProperty('categoria') || !categorias.includes(req.body.categoria)) throw 'No se ha definido una categoría váida';
if (!req.body.hasOwnProperty('fecha')) throw 'No se ha definido una fecha';
await coleccionGastos.insertOne({
viajeId: ObjectId(req.params.viajeId),
descripcion: req.body.descripcion,
cantidad: req.body.cantidad,
categoria: req.body.categoria,
fecha: req.body.fecha
});
return res.status(201).json({ success: true });
} catch (error) {
return res.status(400).json({ success: false, error});
}
}
async function obtenerGastosViaje(coleccionViajes, coleccionGastos, req, res)
{
try {
if (!ObjectId.isValid(req.params.viajeId)) throw 'El viaje indicado no es válido';;
const viaje = await coleccionViajes.findOne(ObjectId(req.params.viajeId));
if (!viaje) throw 'No se ha encontrado el viaje indicado';;
const gastos = await coleccionGastos.find({ viajeId: ObjectId(req.params.viajeId) }).toArray();
return res.status(200).json({success: true, gastos});
} catch (error) {
return res.status(400).json({ success: false, error});
}
}
app.post('/viajes', jsonParser, (req, res) => {
return insertarViaje(coleccionViajes, req, res);
});
app.get('/viajes', (req, res) => {
return obtenerViajes(coleccionViajes, req, res);
});
app.post('/viajes/:viajeId/gastos', jsonParser, (req, res) => {
return insertarGastoViaje(coleccionViajes, coleccionGastos, req, res);
});
app.get('/viajes/:viajeId/gastos', (req, res) => {
return obtenerGastosViaje(coleccionViajes, coleccionGastos, req, res);
});
app.listen(3000, () => console.log('Servidor iniciado'));
Recuerda que también puedes consultar el código completo en el repositorio de GitHub.
Como posible mejora a esta API, estarían las opciones de eliminar gastos o actualizar viajes. También podrías crear una interfaz usando Vue o React. Si nunca has usado estas librerías puedes consultar estos tutoriales:
- Aprende React: Tutorial de introducción a React
Aprender Vue: Tutorial de introducción a Vue
También puedes incluir un sistema de autenticación, de modo que la aplicación puede ser usada por más de un usuario. Para ello necesitarás trabajar con cookies y también saber qué es un token JWT y cómo se usa.
Esto ha sido todo.
Hola Eduardo:
Soy Ignacio. Soy nuevo en estos temas….
Estoy teniendo problemas para hacer los querys.
La aplicación funciona pues si hago un GET 127.0.0.1:3000/viajes
me devuelve
{
«success»: true,
«viajes»: []
}
Si hago POST 127.0.0.1:3000/viajes?nombre:=Roma
me devuelve
{
«success»: false,
«error»: «No se ha definido un nombre»
}
El único cambio que hice en la app es que la base de MongoDB es la de la nube (no local).
No me da errores de conexión a la misma.
Ouede ser un problema de sintaxis ??
Un saludo
Ignacio