Cómo crear una aplicación con el stack MERN

Cómo crear una aplicación con el stack MERN

Como hemos hablado en otras oportunidades, utilizar un stack para el desarrollo de aplicaciones tiene varias ventajas. Entre ellas podemos mencionar la disponibilidad de recursos que facilitan las tareas clásicas (autenticación, enrutamiento, consultas a bases de datos, servidor web de prueba, etc) y la posibilidad de utilizar componentes que se acoplan bien entre sí. Esto significa que existe documentación y soporte profesional o de la comunidad. En esta guía aprenderás cómo crear una aplicación con el stack MERN (MongoDB, Express, React y Node).

Requisitos previos

Creación de la API con Express

Para poder facilitar la comunicación de la API con la base de datos, utilizaremos mongoose como ODM (Object-Document Mapping). Esta herramienta permite hacer las consultas necesarias a través de un mapeo entre los objetos del lado de la API y los documentos de la base de datos. Además, recurriremos a dotenv para leer el detalle de conexión a la base de datos desde un archivo .env y cors para especificar los orígenes válidos. Recuerda que CORS es un mecanismo que permite indicarle al navegador las fuentes legítimas que podrán acceder a un determinado recurso.

Luego de asegurarte de estar en el directorio app, instala los paquetes como mostramos abajo:

cd ~/app
pwd
npm install mongoose dotenv cors

A continuación, crea los directorios routes and models y los archivos routes.js y Usuario.js dentro de cada uno, respectivamente:

mkdir routes
touch routes/routes.js
mkdir models
touch models/Usuario.js

Dentro de routes/routes.js inserta las siguientes líneas. Las rutas en una API nos permiten especificar qué debe suceder cuando llega una solicitud a un endpoint determinado y qué tipos de peticiones permitiremos.

nano routes/routes.js
// Inicializar el router
const express = require('express');
const router = express.Router();

// Importar el modelo Usuario
const Usuario = require('../models/Usuario');

// GET (todos los registros)
router.get('/todos', async (req, res) => {
	try {
		const data = await Usuario.find();
		res.send(data);
	}
	catch (error) {
		res.status(400).json({ message: error.message });
	}
});

// GET (un solo registro)
router.get('/usuario/:id', async (req, res) => {
	try {
		const id = req.params.id;
		const result = await Usuario.findOne({_id: id});
		res.send(result);
	}
	catch (error) {
		res.status(400).json({ message: error.message });
	}
});

// POST (agregar un nuevo registro)
router.post('/agregar', async (req, res) => {
	const { nombre, edad, estado } = req.body;
	const data = new Usuario({
		nombre,
		edad,
		estado,
	});
	try {
		const savedData = await data.save();
		res.send(savedData);
	}
	catch (error) {
		res.status(400).json({ message: error.message });
	}		
});

// PATCH (actualizar un registro)
router.patch('/actualizar/:id', async (req, res) => {
	try {
		const id = req.params.id;
		const updatedData = req.body;
		Usuario.findOneAndUpdate(
      { _id: id },
      { ...updatedData },
      { returnDocument: 'after' },
      (err, result) => {
        if (err) throw 'Hubo un problema al actualizar el registro';
        res.send(result);
      });
	}
	catch (error) {
		res.status(400).json({ message: error.message });
	}
});			

module.exports = router;

Algunos comentarios sobre el código de arriba:

  • GET, POST y PATCH son tres de los métodos HTTP más utilizados. Al usarlos mediante el router que provee Express puedes solicitar registros de la base de datos, insertar nuevos y actualizar los existentes.
  • Las rutas /todos, /usuario/:id, /actualizar/:id y /agregar indican las acciones que queremos llevar a cabo cuando reciben una solicitud. Los casos que incluyen :id requiren que la petición vaya acompañada de un identificador. Los IDs de la colección usuarios están disponibles con la consulta db.usuarios.find() desde la consola de MongoDB.
  • Las funciones callback async (req, res) => { ... } tienen dos parámetros que aquí hemos llamado req (de request o solicitud) y res (response o respuesta). De esta forma, es posible acceder a los parámetros o al cuerpo de la petición (req.params y req.body, respectivamente) y a métodos de la respuesta (send para devolver los datos y status para indicar el estado).

Ahora ingresa el siguiente contenido en models/Usuario.js:

const mongoose = require('mongoose');

const dataSchema = new mongoose.Schema({
	nombre: {
		required: true,
		type: String,
	},
	edad: {
		required: true,
		type: Number,
	},
	estado: {
		required: true,
		type: String,
	},
});

// Relacionar modelo con la colección usuarios
module.exports = mongoose.model('Usuario', dataSchema);

En este archivo acabas de definir la estructura fundamental de los documentos de usuarios. Cada registro está compuesto por tres campos requeridos (puedes agregar más si lo deseas e incluir otros opcionales) de tipos numéricos y de texto. Además, este esquema también admite validaciones y valores por defecto. De todas formas, el contenido de arriba es suficiente para los propósitos de esta guía.

Generar el índice de la API

Toda API tiene un punto de entrada o manera de iniciarla y de especificar parámetros generales. Para este caso crearemos un archivo con el nombre index.js dentro del directorio actual con el contenido que aparece a continuación:

cd ~/app
nano index.js
// Importar herramientas
const express = require('express');
const mongoose = require('mongoose');
const routes = require('./routes/routes');
const cors = require('cors');

// Cargar el archivo con las variables de entorno
require('dotenv').config()

// Obtener el puerto desde el archivo .env
const PORT = process.env.API_PORT

// Cadena de conexión a la base de datos
const dbConnectionString = process.env.DB_CONNECTION_STRING;

// Conectar a la base de datos
mongoose.connect(dbConnectionString);
const database = mongoose.connection;

// Mensaje de error o éxito
database.on('error', error => {
	console.log(error);
});

database.once('connected', () => {
	console.log('Connected to the database');
});

// Inicializar la API
const app = express();
app.use(cors({
  origin: [
    process.env.FRONTEND_APP,
    process.env.BUILD_APP,
  ],
}));
app.use(express.json());
app.use('/api', routes);
app.listen(PORT, () => {
	console.log(`El servidor está escuchando en el puerto ${PORT}`);
});

En el código observarás el uso de process.env, que es una variable global donde Node pone a nuestra disposición la configuración del entorno. Cada línea de la forma VARIABLE=valor que coloquemos en el archivo .env agregará una variable al ambiente para nuestro uso.

nano .env
DB_CONNECTION_STRING='mongodb://USER:[email protected]:27017/academy'
API_PORT=8000
FRONTEND_APP='http://localhost:3000'
BUILD_APP='http://localhost'

Antes de continuar, reemplaza USER y PASS por las credenciales que elegiste en el paso 3 de Cómo instalar el stack MERN (MongoDB, Express, React y Node) en Ubuntu 20.04. Por precaución, cambia los permisos del archivo para que solamente tú puedas leer y escribir sobre este:

chmod 600 .env

Si todo salió como esperábamos, ahora puedes iniciar y manejar la API con PM2 tal como aprendiste en Instalar y configurar Node JS para una aplicación en producción sobre Ubuntu 20.04:

npm install pm2@latest -g
pm2 start index.js

El resultado debería ser similar al que observas en la imagen:

como crear una aplicacion con el stack mern 1
Cómo crear una aplicación con el stack MERN

Una consulta inicial al endpoint api/todos con curl debería devolver los documentos que hemos ingresado previamente:

curl -v http://localhost:8000/api/todos | json_pp
como crear una aplicacion con el stack mern 2
Cómo crear una aplicación con el stack MERN

Para deshabilitar a Rafa, puedes hacer lo siguiente:

curl -X PATCH http://localhost:8000/api/actualizar/62c9a28496c45267d5818fb0 -H "Content-Type: application/json" -d '{"edad": 27, "estado": "Inhabilitado"}' | json_pp 
como crear una aplicacion con el stack mern 3
Cómo crear una aplicación con el stack MERN

Haber utilizado la opción returnDocument: 'after' en la ruta de actualización sirve para pedirle a mongoose que devuelva el documento actualizado. De no especificar esta opción, returnDocument por defecto es false y entrega el registro original.

Agregar el frontend con React

Lo más importante que debes recordar al trabajar con React es que te habilita para desarrollar componentes reutilizables. Dentro del directorio app encontrarás una carpeta con el nombre de src. Allí es donde crearemos un subdirectorio llamado componentes y dentro de este dos archivos para el frontend de nuestra aplicación. El primero de ellos será ListaUsuarios.js y el segundo Usuario.js con los contenidos que indicamos abajo. Recuerda cambiar TU.IP.VA.AQUI con la dirección de tu VPS o localhost si estás ensayando en tu propio equipo.

mkdir src/componentes
nano src/componentes/ListaUsuarios.js
import React, { useState, useEffect } from 'react';
import Usuario from './Usuario';

const ListaUsuarios = () => {
  const [usuarios, setUsuarios] = useState([]);

  useEffect(() => {
    const obtenerUsuarios = async () => {
      const respuesta = await fetch('http://TU.IP.VA.AQUI:8000/api/todos');
      if (respuesta) {
        const datosUsuarios = await respuesta.json();
        setUsuarios(datosUsuarios);
      }
    }

    obtenerUsuarios();
  }, []);

  return (
    <ul>
      {usuarios.map((usuario, index) =>
        <Usuario
          { ...usuario }
          key={index}
        />
      )}
    </ul>
  );
}

export default ListaUsuarios;

En el código de arriba hacemos una llamada a la API utilizando una función asíncrona y guardamos la respuesta en el estado usuarios a través de useState. Gracias a useEffect con su array de dependencias vacío, esta operación se realiza una sola vez luego de que el componente ListaUsuarios se renderiza.

A continuación te presentamos el código del componente Usuario:

nano src/componentes/Usuario.js
import React from 'react';

const Usuario = ({ nombre, edad, estado }) => {

  const handleClick = (e, estado) => {
    console.log(estado);
  };

  return (
    <li onClick={e => handleClick(e, estado)}>
      {`${nombre} tiene ${edad} años y se encuentra ${estado}`}
    </li>
  );

};

export default Usuario;

Como puedes apreciar, cada usuario aparecerá dentro de una lista y al hacer clic sobre cada uno se mostrará por consola su estado. Aquí es donde podemos observar una de las bondades de React. Al dedicar un componente a la representación de usuarios individuales es posible hacer cambios de una vez en solamente un lugar. Estas modificaciones entran en efecto de manera automática para a cada elemento de la lista.

Los últimos pasos para poder ver tu tarea por pantalla son 1) editar el archivo App.js dentro de src para que quede como indicamos a continuación y 2) iniciar la aplicación en modo desarrollo luego de guardar los cambios.

nano src/App.js
import logo from './logo.svg';
import './App.css';
import ListaUsuarios from './componentes/ListaUsuarios';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <ListaUsuarios />
      </header>
    </div>
  );
}

export default App;
npm start
como crear una aplicacion con el stack mern 4
Cómo crear una aplicación con el stack MERN

Debido a que la aplicación continuará corriendo mientras no interrumpamos el comando npm start, ahora debemos encontrar una forma más eficaz de servirla.

Generar un build y servir la aplicación con Nginx

Para finalizar, genera el build de producción y cópialo al directorio por defecto de Nginx:

npm run build

Este recurso contiene el resultado de generar archivos JavaScript, CSS y HTML por separado a partir del código JSX de React. De esta manera, Nginx podrá servir el contenido y tu navegador entenderlo sin problemas.

pwd # Confirmar que todavía estemos dentro del directorio app
ls -l build/
sudo cp -r build/* /var/www/html

Ahora es posible acceder a la aplicación sin especificar el puerto y confirmar que Nginx está operando detrás de escenas:

como crear una aplicacion con el stack mern 5
Cómo crear una aplicación con el stack MERN
sudo tail -n 5 /var/log/nginx/access.log
como crear una aplicacion con el stack mern 6
Cómo crear una aplicación con el stack MERN

Existen otras formas para desplegar aplicaciones a producción. Este ejemplo muestra solamente una de ellas (por cierto, la más rudimentaria y menos automática). Si te interesa profundizar al respecto, consulta la documentación de GitHub Actions.

Conclusión

En este artículo aprendiste cómo crear una aplicación con el stack MERN y desplegarla a producción. La documentación de MongoDB, Express, React y Node es rica en ejemplos para agregar funcionalidad y mejorar el código inicial que compartimos. Lo mismo aplica para JavaScript, sobre lo cual puedes leer más en el sitio de MDN.

Gabriel Cánepa
Gabriel Cánepa

Gabriel trabaja actualmente como desarrollador full-stack en Scalar, una firma que se dedica a hacer valuaciones de empresas. Es Administrador de Sistemas certificado por la Fundación Linux y previamente ha escrito un gran número de artículos y contenidos técnicos sobre el tema para: DigitalOcean, Linode, Carrera Linux Argentina y Tecmint.

Tiene una certificación en programación de la Universidad de Brigham Young-Idaho, y está completando las carreras de programador y analista de sistemas en la Universidad Nacional de Villa Mercedes (UNViMe).

En su tiempo libre, Gabriel disfruta leyendo libros de Stephen R. Covey, tocando piano y guitarra, y enseñando conocimientos de programación a su dos hijas.