API CRUD — Node.js y Express

Mauricio Garcia
15 min readOct 7, 2024

--

Introducción

En esta story, explicaremos cómo crear un conjunto de API’s que permitan gestionar un mensaje con las operaciones básicas de alta, baja, modificación y consulta. El objetivo es construir un CRUD (Create, Read, Update, Delete).

Realizaremos un proyecto que nos permitirá utilizar Express como framework para la creación de las APIs y Node.js como entorno de ejecución. Además, usaremos variables de entorno para hacer que la aplicación sea flexible y configurable en distintos entornos sin modificar el código fuente. A través de este ejemplo, aprenderás a construir un backend simple para usar en aplicaciones web. Finalmente utilizaremos Mongo (MongoDB o Mongoose) para la persistencia de datos.

Antes de comenzar, te sugiero primero revisar:

  • Node.js, NPM, Yarn, NVM, Webpack, Babel…(I)
  • Express (I, II, III, IV, V, VI)

Configurando la Aplicación

El primer paso será crear la estructura de la aplicación. Para ello vamos a crear una carpeta llamada blog. Los comandos serían los siguientes:

> mkdir blog
> cd blog

:: Gestión de dependencias

Recordemos que todas las dependencias del proyecto se manejan a través de un archivo llamado package.json. Este archivo define las bibliotecas y versiones que la aplicación necesita para funcionar. Podemos generar el archivo de diferentes maneras, para este caso usaremos npm. En la consola ejecutamos el siguiente comando:

npm init

Este comando te guiará a través de una serie de preguntas para configurar el archivo package.json.

Si deseas aprender más sobre cómo configurar el archivo de manera óptima y segura, te recomiendo consultar la documentación [npm-init] [package.json] o con el siguiente comando npm help json desde la terminal.

Para el proyecto, vamos a dejar los valores por defecto que nos ofrece, excepto el nombre del archivo principal. A este le asignaremos el nombre de app.js en lugar del valor por defecto (index.js).

Al finalizar el proceso, se generará el archivo en la raíz del proyecto. Este debe verse más o menos así:

{
"name": "blog",
"version": "1.0.0",
"description": "My first MEN",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"mongodb",
"exprress",
"nodejs"
],
"author": "mauriciogc.medium.com",
"license": "ISC"
}

Con esta configuración, ya estaremos listos para instalar las dependencias necesarias y comenzar a construir la aplicación.

:: Creando el Archivo Principal de la Aplicación

Una vez que se hemos creado y configurado el package.json, vamos a proceder a crear la estructura básica de la aplicación.

Primero, creamos una carpeta llamada src dentro del proyecto. Posteriormente, dentro de la carpeta src, creamos el archivo app.js, que será el punto de entrada a la aplicación (tal como lo configuramos)

La estructura del proyecto debería verse así:

/blog
/src
app.js
package.json

Dentro del archivo app.js, agregamos una simple consola para verificar que todo está funcionando correctamente:

console.log("Hello Blog");

En consola ejecutamos el siguiente comando:

node src/app.js

Si todo está en orden, deberías ver el siguiente mensaje en la consola:

:: Configurando Node.js con Nodemon y Babel

— Nodemon

Hasta ahora, cada vez que levantamos el servidor de las aplicaciones que se han hecho con Node.js y Express, lo hemos hecho con el comando node src/app.js. Esto implica que cada vez que hacemos un cambio en el código, debemos detener el servidor manualmente y volver a iniciarlo, seamos sinceros, es bastante tedioso y propenso a errores (en ocasiones podríamos olvidar reiniciar el servidor, lo que puede llevarnos a perder tiempo intentando identificar por que los cambios no se reflejan).

Para resolver este problema y automatizar el reinicio del servidor, vamos a instalar [Nodemon], una herramienta que se encarga de reiniciar automáticamente la aplicación cada vez que detecta cambios en los archivos.

Dentro del proyecto, ejecuta el siguiente comando para instalar Nodemon como una dependencia de desarrollo:

npm install --save-dev nodemon

Una vez instalado, vamos a actualizar el archivo package.json para automatizar el uso de Nodemon. Agregamos un script que al ejecutar el comando npm start, se levante el servidor automáticamente.

En la propiedad scripts, agregamos lo siguiente:

"scripts": {
"start": "nodemon src/app.js"
}

Para probarlo, solo hay que ejecutar el siguiente comando en la terminal:

npm start

Si haces los cambios en tú código, verás que Nodemon reinicia automáticamente el servidor, lo que te permitirá ahorrar tiempo y evitar errores.

— Babel

Node.js, aunque está en constante actualización, no siempre incluye todas las características más recientes de ECMAScript. Para asegurarnos de que la aplicación pueda usar las últimas funcionalidades de JS, vamos a instalar [Babel].

Dentro del proyecto, ejecuta el siguiente comando para instalar los paquetes necesarios para Babel:

npm install --save-dev @babel/core @babel/node @babel/preset-env
  • @babel/core: El núcleo del compilador de Babel.
  • @babel/node: Una CLI que funciona como node, pero que compila los presets y plugins de Babel antes de ejecutar el código.
  • @babel/preset-env: Un preset que te permite utilizar las últimas características de JavaScript sin preocuparte por el soporte en diferentes entornos.

En la raíz del proyecto, crea el archivo .babelrc y añade lo siguiente:

{
"presets": ["@babel/preset-env"]
}

Esto le indica a Babel que use el preset @babel/preset-env para que compile el código de JS moderno.

Ahora debemos actualizar el archivo package.json para que Babel sea parte del flujo de trabajo al ejecutar el servidor. Cambiamos el script de start para que Babel compile el código antes de que Nodemon lo ejecute:

"scripts": {
"start": "nodemon --exec babel-node src/app.js"
}

Si todo está configurado correctamente, al ejecutar npm start, Nodemon iniciará el servidor y Babel se encargará de compilar el código para que sea compatible con todas las versiones de Node.js.

:: Configurando express

El siguiente paso es instalar Express, que será el framework que utilizaremos para crear la API.

Para instalar Express, ejecuta el siguiente comando en la terminal dentro del proyecto:

npm install express

Una vez que Express se ha instalado, vamos a modificar el archivo src/app.js para levantar un servidor muy básico. Abre el archivo app.js y cambia el código que tiene por el siguiente:

const express = require('express');
const app = express();
const port = 3000;

// Ruta básica
app.get('/', (req, res) => {
res.send('¡Hola, mundo desde Express!');
});

// Levantar servidor
app.listen(port, () => {
console.log(`Servidor escuchando en http://localhost:${port}`);
});

Si ya tienes Nodemon configurado y el servidor está en ejecución, al guardar los cambios en app.js, Nodemon detectará las modificaciones y reiniciará automáticamente el servidor. Para comprobar que todo está funcionando correctamente, abre tu navegador y visita la siguiente URL:

http://localhost:3000

Si has seguido los pasos correctamente, deberías ver lo siguiente:

:: Configurando el CORS

Al crear una API REST [ref] con Express y consumirla desde algún navegador, es muy común (más de lo que te imaginas) encontrarse con el siguiente error:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:3000/. (Reason: CORS header 'Access-Control-Allow-Origin' missing).
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:3000/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).

Este error ocurre cuando intentamos acceder a un recurso en un dominio diferente al que la aplicación solicita. Por ejemplo:

La API está alojada en https://my-api.com y la aplicación que intenta acceder a la API está en https://my-app.com.

En este caso, estamos tratando de acceder desde my-app.com a recursos de my-api.com, lo cual genera un conflicto de seguridad por las políticas de mismo origen.

La solución para este problema es configurar correctamente CORS en el servidor Express, lo que permitirá que las solicitudes desde diferentes dominios se realicen sin problemas.

CORS (Cross-Origin Resource Sharing) es un mecanismo que permite que una página web solicite recursos restringidos de otro dominio. [ref] [ref].

Vamos a usar el paquete cors[ref]. Ejecuta el siguiente comando en la consola para instalarlo:

npm install cors

Para una configuración básica en [Express], debemos incluir y usar el paquete CORS en el archivo src/app.js. Esto permitirá que cualquier dominio pueda acceder a los recursos (aunque es recomendable personalizar esto en un entorno de producción para mayor seguridad).

Abre el archivo src/app.js y agrega el siguiente código:

const express = require('express');
// Importamos CORS
const cors = require('cors');
const app = express();

// Habilitando CORS para todas las solicitudes
app.use(cors());

app.get('/', (req, res) => {
res.send('¡Hola, mundo desde Express!');
});

app.listen(port, () => {
console.log(`Servidor escuchando en http://localhost:${port}`);
});

Con este código, hemos habilitado CORS para todas las solicitudes a el servidor, lo que significa que cualquier dominio podrá acceder a los recursos. Si quieres limitar el acceso a un dominio específico (recomendable), puedes hacer una configuración más detallada [ref][ref] .

:: Configurando y creando variables de entorno

En el desarrollo real de aplicaciones, es fundamental que estas puedan ejecutarse en diferentes entornos, ya sea en la máquina de un compañero, en un servidor local o en la nube. Para garantizar que la aplicación se comporte correctamente en cada uno de los entornos sin necesidad de modificar código, utilizamos variables de entorno.

Las variables de entorno permiten que la configuración de la aplicación cambie según el entorno en el que se ejecute, sin necesidad de regenerar o modificar el código fuente.

— ¿Cuándo usar variables de entorno?

Los casos más comunes:

  • El puerto HTTP en el que la aplicación escuchará.
  • Las rutas de archivos o carpetas específicas.
  • La URL y configuraciones de la base de datos.

Cualquier aplicación que requiera configuraciones específicas debe utilizar variables de entorno.

Cuando Node.js inicia un proceso, proporciona un objeto global llamado process.env[ref], que contiene todas las variables de entorno disponibles en el sistema. Para ver todas las variables de entorno, podemos imprimir este objeto en la consola:

console.log(process.env);

Abre el archivo app.js, añade el código de arriba y revisa la consola.

Para acceder a una variable en específico, podemos hacerlo como cualquier propiedad de un objeto.

console.log(process.env.USER);

— Accediendo a las variables de Entorno.

Existen dos maneras de acceder a variables de entorno en Node.js:

  • Desde la línea de comandos: Puedes pasar variables directamente al iniciar la aplicación, por ejemplo:
PORT=5000 node src/app.js
  • Desde un archivo .env : Esta es la forma más común, recomendada y sencilla en aplicaciones.

— Por archivo .env

Vamos a utilizar un archivo .env para definir las variables de entorno. Esto permite que la configuración esté centralizada y sea fácil de modificar sin tocar el código fuente.

En la raíz del proyecto, crea el archivo .env y define las variables de entorno de la siguiente manera:

PORT=3000

Esto define que la aplicación utilizará el puerto 3000. Puedes agregar tantas variables según lo necesites.

— Instalando el paquete dotenv

Para que la aplicación de Node.js lea las variables de entorno desde el archivo .env, necesitamos instalar el paquete dotenv[ref].

Ejecuta el siguiente comando en la consola para instalarlo:

npm install dotenv

Una vez instalado, modifica el archivo src/app.js para utilizar las variables de entorno desde el archivo .env. Añade las siguientes líneas al archivo:

// Importamos dotenv para cargar las variables de entorno
require('dotenv').config();

const express = require('express');
const cors = require('cors');
const app = express();

// Utilizamos la variable de entorno para el puerto
const port = process.env.PORT || 3000;

app.use(cors());

app.get('/', (req, res) => {
res.send('¡Hola, mundo desde Express!');
});

app.listen(port, () => {
console.log(`Servidor escuchando en http://localhost:${port}`);
});

Es importante no subir el archivo .env al repositorio, ya que puede contener información sensible como credenciales de bases de datos o claves API. Para ello, debemos agregarlo al archivo .gitignore.

Si no tienes el archivo .gitignore en la raíz del proyecto, crealo y agrega lo siguiente:

.env

Se recomienda crear un archivo .env.dist con las variables de entorno vacías, para que otros desarrolladores que descarguen el proyecto puedan rellenar con sus propias variables. Siguiendo el ejemplo, el archivo .env.dist podría verse así:

PORT=

Si has seguido los pasos correctamente, levanta el servidor (npm start) y deberás ver lo siguiente:

Si estás usando Visual Code, te recomiendo instalar la extensión [nc-dotenv] para tener resaltado la sintaxis en los archivos .env. Esto hará que el archivo sea más legible y fácil de mantener.

— Descarga el código

Github[tag: 0.1.0][ref].

Creando API’s

Vamos a generar las siguientes peticiones para gestionar los mensajes:

Donde:

  • GET /messages/ — Ruta que permite obtener todos los mensajes almacenados en la base de datos. No requiere parámetros adicionales.
  • GET /messages/:messageId — Ruta para obtener un mensaje específico, utilizando el messageId como parámetro de la URL.
  • POST /messages/ — Ruta que permite agregar un nuevo mensaje. Los datos del mensaje deben enviarse en el cuerpo de la solicitud en formato JSON, con la estructura { "message": "..." }.
  • DELETE /messages/:messageId — Ruta que permite eliminar un mensaje específico utilizando el messageId como parámetro de la URL.

:: Definiendo y configurando las rutas

Modifica el archivo src/app.js para agregar las peticiones. Añade las siguientes líneas al archivo:

Código inicial:

require('dotenv').config();
const express = require('express');
const cors = require('cors');

const app = express();
const port = process.env.PORT || 3000;

//-- Middlewares
app.use(cors());
app.use(express.json()); // Permite trabajar con JSON en las solicitudes
app.use(express.urlencoded({ extended: true })); // Soporta datos codificados en URL

//-- Iniciar el servidor
app.listen(port, () => {
console.log(`Servidor escuchando en http://localhost:${port}`);
});

Antes del inicio del servidor, añadimos cada una de las peticiones, donde se retorna un mensaje básico:

//-- Middlewares
// ...

//-- Peticiones
// Ruta GET para obtener todos los mensajes
app.get("/messages", (req, res) => {
return res.send("GET HTTP method to /messages ");
});

// Ruta GET para obtener un mensaje específico por ID
app.get("/messages/:messageId", (req, res) => {
console.log(req.params);
return res.send("GET HTTP method to /messages/:messageId");
});

// Ruta POST para agregar un nuevo mensaje
app.post("/messages", (req, res) => {
console.log(req.body.message); // Mostramos el contenido del mensaje en consola
return res.send("POST HTTP method to /messages/ ");
});

// Ruta DELETE para eliminar un mensaje por ID
app.delete("/messages/:messageId", (req, res) => {
console.log(req.params); // Mostramos el ID del mensaje que se va a eliminar
return res.send("DELETE HTTP method to /messages/:messageId");
});

//-- Iniciar el servidor
// ...

Para asegurarnos de que las rutas están funcionando correctamente, vamos a utilizar [Thunder Client], que es un plugin de VSCode.

Thunder Client es una extensión que nos va a permitir hacer peticiones HTTP directamente desde VSCode, facilitando las pruebas de las APIs sin necesidad de abrir una aplicación externa como Postman. Bastante útil cuando estamos desarrollando y queremos realizar pruebas rápidas sin salir del entorno de desarrollo.

Dentro de Thunder Client, vamos a generar una colección de peticiones, con los siguientes request:

  • GET http://localhost:3000/messages para obtener todos los mensajes.
  • GET http://localhost:3000/messages/:messageId para obtener un mensaje.
  • POST http://localhost:3000/messages para agregar un nuevo mensaje (en el body, añade un campo message con un texto).
  • DELETE http://localhost:3000/messages/:messageId para eliminar un mensaje.

Si has seguido los pasos correctamente, levanta el servidor (npm start) y deberás ver lo siguiente:

:: Modularizando las rutas con express.Router

Vamos a modularizar el código utilizando express.Router[ref] para mantener el archivo principal más limpio y organizado.

En lugar de definir todas las rutas directamente en el archivo principal app.js, crearemos un nuevo archivo src/routes/messages.js, que contendrá todas las rutas relacionadas con los mensajes.

Dentro de este archivo, definiremos las rutas para manejar las solicitudes de mensajes. Primero, necesitamos importar Express y su enrutador:

const express = require("express");
const router = express.Router();

Vamos a añadir un middleware que registre el método HTTP y la URL de todas las solicitudes realizadas a las rutas de mensajes. Este middleware se ejecutará antes de todas las rutas:

router.use((req, res, next) => {
console.log(`Request method: ${req.method}, Request URL: ${req.originalUrl}`);
next();
});

Ahora vamos a traer cada una de las rutas que teníamos en app.js, pero adaptándolas para que funcionen con express.Router:

// Obtener todos los mensajes
router.get("/", (req, res) => {
return res.send("GET HTTP method to /messages ");
});

// Obtener un mensaje específico por ID
router.get("/:messageId", (req, res) => {
console.log(req.params);
return res.send("GET HTTP method to /messages/:messageId");
});

// Agregar un nuevo mensaje
router.post("/", (req, res) => {
console.log(req.body.message);
return res.send("POST HTTP method to /messages/ ");
});

// Eliminar un mensaje por ID
router.delete("/:messageId", (req, res) => {
console.log(req.params);
return res.send("DELETE HTTP method to /messages/:messageId");
});

Finalmente, exportamos el enrutador para poder utilizarlo en el archivo principal app.js:

module.exports = router;

En el archivo app.js hacemos los siguientes cambios:

require('dotenv').config();
const express = require('express');
const cors = require('cors');

const app = express();
const port = process.env.PORT || 3000;

//-- Middlewares Globales
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

//-- Importar las rutas
// Importamos la ruta de messages
const messageRoutes = require('./routes/messages');

//-- Montamos las rutas
// "/messages"
app.use('/messages', messageRoutes);

app.listen(port, () => {
console.log(`Servidor escuchando en http://localhost:${port}`);
});

Este cambio nos permitirá mantener el archivo principal app.js más limpio y modular, separando las rutas en diferentes archivos. Esta modularización es muy útil para mantener el proyecto organizado a medida que crece, ya que facilita la adición de nuevas funcionalidades o rutas sin aumentar la complejidad del archivo app.js.

Si has seguido los pasos correctamente, las peticiones deberán de seguir funcionando sin ningún problema.

:: Base de datos de mensajes

— Simulando los datos

Antes de obtener los datos de la base, primero vamos a simularlos creando un JSON mock. En el archivo routes/messages.js, creamos la constante messages para almacenar mensajes en memoria (más adelante será reemplazado por una base de datos en MongoDB).

// Simulación de base de datos de mensajes
const messages = [];

Actualizamos cada una de las peticiones con retorno de datos en formato JSON.

  • GET http://localhost:3000/messages — Retornamos la lista de mensajes.
// Obtener todos los mensajes
router.get('/', (req, res) => {
return res.json(messages);
});
  • GET http://localhost:3000/messages/:messageId — Validamos si el messageId existe en el arreglo de mensajes; si no se encuentra, enviamos un error controlado. En caso contrario, retornamos el mensaje correspondiente.
// Obtener un mensaje específico por ID
router.get('/:messageId', (req, res, next) => {
const { messageId } = req.params;
const message = messages.find((msg) => messageId === msg.id);

if (!message) {
const error = new Error('Mensaje no encontrado.');
error.status = 404;

//Para el error al middleware general de manejo de errores
return next(error);
}

return res.json(message);
});
  • POST http://localhost:3000/messages — Validamos si en el cuerpo de la solicitud body viene la propiedad message; si no se encuentra, enviamos un error controlado. En caso contrario, insertamos el nuevo mensaje y retornamos el mensaje insertado.
// Agregar un nuevo mensaje
router.post('/', (req, res, next) => {
const { message } = req.body;
if (!message) {
const error = new Error('No se ha encontrado la propiedad message');
error.status = 404;
//Para el error al middleware general de manejo de errores
return next(error);
}

const newMessage = { id: `${messages.length + 1}`, message };
messages.push(newMessage);
return res.status(201).json(newMessage);
});
  • DELETE http://localhost:3000/messages/:messageId — Validamos si el messageId existe en el arreglo de mensajes; si no se encuentra, enviamos un error controlado. En caso contrario, eliminamos el mensaje y retornamos que se ha eliminado.
// Eliminar un mensaje por ID
router.delete('/:messageId', (req, res, next) => {
const { messageId } = req.params;
const index = messages.findIndex((msg) => msg.id === messageId);
if (index === -1) {
const error = new Error('Mensaje no encontrado.');
error.status = 404;
//Para el error al middleware general de manejo de errores
return next(error);
}

messages.splice(index, 1);
return res.send(`Mensaje con ID ${messageId} eliminado.`);
});

Finalmente, en el archivo app.js, antes de iniciar el servidor, añadimos el middleware de manejo de errores. El objetivo de este código será capturar cualquier error que ocurra en la aplicación, enviando una respuesta JSON con el mensaje de error:

// Middleware de manejo de errores centralizado
app.use((err, req, res, next) => {
console.error(err.stack); // Mostrar el error en la consola
res.status(err.status || 500).json({
message: err.message || "Error interno del servidor",
error: err,
});
});

Si has seguido los pasos correctamente, levanta el servidor (npm start) y deberás ver lo siguiente:

Importante saber que cada vez que se reinicia el servidor, se pierden todos los datos, ya que están almacenados en memoria.

— Descarga el código

Github[tag: 0.2.0][ref].

Conclusión

Con esta story implementamos una versión funcional del CRUD con una simulación de datos en memoria. Es decir, aunque la API funciona correctamente, los datos se pierden cada vez que se reinicia el servidor, ya que no hay persistencia en una base de datos.

:: Próximos Pasos

Para asegurar la persistencia de los datos, podemos implementar una conexión a MongoDB. A continuación, tienes dos caminos para hacerlo:

Con mongodb (Driver nativo de MongoDB):

  • Ventaja: Control sobre las operaciones de la base de datos. Es ideal si requieres una mayor flexibilidad en las consultas y control a bajo nivel.
  • Desventaja: Puede requerir más código repetitivo para tareas comunes.

Con mongoose (ODM para MongoDB):

  • Ventaja: Proporciona una capa que simplifica la manipulación de los datos, proporciona esquemas de datos, validaciones automáticas, y un enfoque orientado a objetos. Es ideal para la mayoría de las aplicaciones.
  • Desventaja: Añade cierta sobrecarga que puede no ser necesario para proyectos básicos con interacciones básicas con MongoDB.

Haz clic en una de las siguientes opciones para continuar:

--

--

No responses yet