la35.net blog para alumnos de la ET Nº35

Hello database

Ya está la segunda guía de full stack MERN en santiagotrini/hello-database.

En esta guía les muestro como usar MongoDB e integrarlo a una app en Heroku para hacer una API super básica.

Para la API en Express usamos Mongoose como ODM (object document mapper).

Contenidos

  1. Qué vamos a hacer
  2. Antes de empezar
  3. Creando el proyecto
  4. Mandamos JavaScript
  5. Creando la base de datos
  6. Las rutas de la API
  7. Haciendo queries
  8. Creando la base de datos (otra vez)
  9. Agregar IPs a la whitelist de MongoDB Atlas
  10. Conectarse e insertar los usuarios
  11. Decirle a Heroku donde está la base de datos
  12. Agregar CORS a la API
  13. ¿Y ahora?
  14. Guías, referencias y documentación

Qué vamos a hacer

Una API (application programming interface) para comunicarnos con una base de datos de MongoDB. No voy a explicar que es una base de datos, ya tienen una materia entera para eso.

Este es uno de los usos más comunes de Express. Una API nos da una interfaz para comunicar una aplicación web (frontend) con una base de datos. Tanto la base de datos como la aplicación de Express se consideran parte del backend.

El ejemplo va a ser mínimo, tenemos una base de datos con usuarios y a través de la API podemos obtener esa información en formato JSON. Ya tendremos tiempo de complicar más las cosas.

Antes de empezar

Para este ejemplo necesitamos lo mismo que en express-hello-world y dos cosas más:

Cuando instalen el server de MongoDB en Windows pueden omitir la instalación de MongoDB Compass, hay un checkbox en el instalador. No lo vamos a usar.

Verificar que podamos acceder a la shell de MongoDB en la terminal. Si esto no funciona es porque no tienen el ejecutable en el PATH. Tarea de investigación: cómo editar variables de entorno en Windows. Concretamente agregar la ruta C:\Program Files\MongoDB\Server\4.2\bin (o algo similar) a la variable de entorno PATH.

$ mongo --version
MongoDB shell version v4.2.7
... etc ...
$ mongo
> exit
bye

Si al ejecutar mongo nos recibe un prompt de Mongo (el >) va todo bien. Pueden salir con exit.

Creando el proyecto

Esto ya lo vimos, en resumen:

$ mkdir hello-database
$ cd hello-database
$ git init
$ npm init -y
$ echo node_modules > .gitignore
$ npm install express
$ touch index.js

Recuerden que npm install requiere Internet. Agregamos una nueva librería para conectarnos y trabajar con bases de datos de MongoDB.

$ npm install mongoose

Agregamos a scripts en el package.json la propiedad "start": "node index.js" (lo mismo que hicimos en express-hello-world).

Mandamos JavaScript

En index.js le decimos a nuestra app que se conecte a la base de datos.

// index.js

// ahora tambien importamos mongoose
const express  = require('express');
const mongoose = require('mongoose');

// puerto y base de datos
const port = process.env.PORT        || 3000;
const db   = process.env.MONGODB_URI || 'mongodb://localhost/hellodb';

const app = express();

// conexion a la base de datos
mongoose.set('useUnifiedTopology', true);
mongoose.set('useFindAndModify', false);
mongoose
  .connect(db, { useNewUrlParser: true })
  .then(() => {
    console.log(`DB connected @ ${db}`);
  })
  .catch(err => console.error(`Connection error ${err}`));

// el server escucha todo
app.listen(port, () => {
  console.log(`Server listening on port ${port}`);
});

Aparecieron algunas cosas nuevas. En primer lugar importamos una librería nueva: Mongoose. En segundo lugar además de la variable que indica el puerto del server tenemos una similar para la base de datos, misma idea, un valor cuando estamos en Heroku y otro cuando ejecutamos localmente. La URI mongodb://localhost/hellodb es la base de datos local en nuestra computadora.

Todo lo que está entre el primer mongoose.set y el .catch() es el snippet de código para conectarnos a una base de datos con Mongoose. Un snippet es un pedazo de código que copiamos prácticamente igual en distintas aplicaciones. En resumen son dos llamadas a la función mongoose.set() para evitar unos warnings (prueben de comentar esas dos líneas). Y una llamada a la función mongoose.connect(). El motivo de que lo vean raro es que usa algo llamado promesas, que en esencia cumple la misma función que una callback.

En app.listen() sí usamos una callback. Es el segundo argumento de la función. Los dos usos de console.log() son para mostrar que todo anduvo bien y que nuestro server esta escuchando en tal puerto y conectado a tal base de datos. Pueden probar la app con npm start y van a ver algo similar a esto.

$ npm start

> [email protected] start /home/santiago/hello-database
> node index.js

Server listening on port 3000
DB connected @ mongodb://localhost/hellodb

Creando la base de datos

Tenemos que crear una base de datos para trabajar. Lo hacemos mediante un sencillo script usando Mongoose. Las bases de datos de MongoDB guardan la información en colecciones de documentos. Las colecciones pueden ser usuarios, animales, compras o lo que ustedes quieran. Los documentos son objetos de JavaScript. En nuestro caso vamos a tener una colección de usuarios, y el objeto que represente un usuario va a tener esta pinta.

{
  id: 1,
  name: 'Juan',
  mail: '[email protected]',
  birthday: '2000-01-14'
}

Cada usuario va a ser un documento en la colección de usuarios. Una colección no es más que un array de objetos con idéntica estructura. Por si todavía no se enteraron la idea acá es que (casi) todo se hace en JavaScript, bases de datos incluídas.

Para que Mongoose interactúe con MongoDB tenemos que crear modelos. Un modelo es un objeto que describe una colección, y lo hacemos generalmente en un archivo separado dentro de una carpeta llamada models. Así que en la terminal.

$ mkdir models
$ cd models
$ touch User.js

La convención es usar SnakeCase y nombres en singular para los nombres de los archivos de los modelos. Si tenemos un modelo de animales será Animal.js. Si tenemos animales de zoológico será ZooAnimal.js. El archivo models/User.js quedaría así.

// User.js

// necesitamos importar mongoose
const mongoose = require('mongoose');

// los modelos se crean a partir de un schema
const UserSchema = new mongoose.Schema({
  id: Number,
  name: String,
  mail: String,
  birthday: Date
});
// el schema describe la pinta de un documento de la coleccion

// creamos el modelo llamando a mongoose.model(nombre, schema)
const User = mongoose.model('User', UserSchema);

// hay que exportar el modelo para usarlo despues en otros archivos
module.exports = User;

El ejemplo de User.js es bien sencillo pero los schemas tienen muchas características más que iremos viendo a medida que las necesitemos. La documentación oficial de Mongoose es la referencia para este tipo de cosas.

Con el modelo listo nos creamos un script llamado populatedb.js en la carpeta del proyecto y copiamos el siguiente código.

// populatedb.js

// necesitamos importar mongoose
const mongoose = require('mongoose');

// importar el modelo de usuario
const User = require('./models/User');

// la URI de la db
const db = 'mongodb://localhost/hellodb';

// array de usuarios para ingresar a la db
const users = [
  {
    id: 1,
    name: 'Juan',
    mail: '[email protected]',
    birthday: '2000-05-24'
  },
  {
    id: 2,
    name: 'Maria',
    mail: '[email protected]',
    birthday: '2000-02-13'
  },
  {
    id: 3,
    name: 'Pedro',
    mail: '[email protected]',
    birthday: '2000-05-19'
  },
  {
    id: 4,
    name: 'Julia',
    mail: '[email protected]',
    birthday: '1998-03-01'
  }
];

// conectarse a la db
mongoose.set('useUnifiedTopology', true);
mongoose.set('useFindAndModify', false);
mongoose
  .connect(db, { useNewUrlParser: true })
  .then(() => {
    // si nos conectamos con exito mostrar mensajes
    // e insertar los usuarios en el array
    console.log(`DB connected @ ${db}`);
    console.log('Populating DB...');
    User.insertMany(users, (err, users) => {
      if (err) throw err;
      // un mensaje con la cantidad de documentos insertados
      console.log(`${users.length} documents inserted!`);
      // cerramos la conexion cuando terminamos
      mongoose.connection.close();
    });
  })
  .catch(err => console.error(`Connection error ${err}`));

No importa si no lo entienden del todo, en resumen lo que hace es conectarse a la base de datos y poner cuatro documentos en la colección users. Cuando termina se desconecta. Lo pueden ejecutar desde la terminal con node populatedb.js.

Chequeamos con la shell de Mongo que el script haya hecho su trabajo.

$ mongo
> show databases
admin    0.000GB
config   0.000GB
hellodb  0.000GB
local    0.000GB
> use hellodb
switched to hellodb
> show collections
users
> db.users.find().pretty()
{
  "_id": ObjectId("5ee..."),
  "id": 1,
  "name": "Juan",
  ... etc ...
}
... etc ...

Nos debería mostrar los cuatro objetos en la colección. Ya que estamos en la shell de Mongo probemos de agregar un usuario más desde ahí. Después salimos de la shell de Mongo con exit.

> db.users.insert({ id: 5, name: "Mario", mail: "[email protected]", birthday: new Date("1995-04-14") })
WriteResult({ "nInserted" : 1 })
> exit
bye

Listo, tenemos base de datos. Pero volvamos a lo que nos importa, hacer una API para esa base de datos.

Las rutas de la API

La idea de hacer una API con Express es poder interactuar con una base de datos por medio de URLs en el navegador (o en alguna aplicación por medio de HTTP).

Para eso usamos rutas, que no son mas que URLs, pero siguiendo algún esquema predecible. Por ser la primera vez lo hacemos super simple. Vamos a tener dos tipos de rutas, una que me devuelva todos los usuarios de la colección users y otro tipo de ruta que me devuelva un usuario particular según el ID del usuario.

En la terminal hacemos unas carpetas y un archivo.

$ mkdir routes
$ cd routes
$ mkdir api
$ cd api
$ touch user.js

En user.js creamos un router y definimos las rutas.

// user.js

// usamos express
const express = require('express');

// creamos un router
const router = express.Router();

// GET a /api/users (todos los usuarios)
router.get('/users', (req, res) => {
  res.send('todos los usuarios');
});

// GET a /api/user/id (un solo usuario)
router.get('/user/:id', (req, res) => {
  res.send('el usuario con id ' + req.params.id);
});

// hay que exportar el router para usarlo en index.js
module.exports = router;

Luego modificamos index.js para usar estas rutas.

// index.js

// ahora tambien importamos mongoose
const express  = require('express');
const mongoose = require('mongoose');

// importamos el router que creamos para la api
const router = require('./routes/api/user');

// puerto y base de datos
const port = process.env.PORT        || 3000;
const db   = process.env.MONGODB_URI || 'mongodb://localhost/hellodb';

const app = express();

// conexion a la base de datos
mongoose.set('useUnifiedTopology', true);
mongoose.set('useFindAndModify', false);
mongoose
  .connect(db, { useNewUrlParser: true })
  .then(() => {
    console.log(`DB connected @ ${db}`);
  })
.catch(err => console.error(`Connection error ${err}`));

// usamos el router
app.use('/api', router);

// el server escucha todo
app.listen(port, () => {
  console.log(`Server listening on port ${port}`);
});

Si ejecutamos el server con npm start y vamos a http://localhost:3000/api/users o http://localhost:3000/api/user/1 podemos verificar que todo funcione.

Claro que falta traer la información de la base de datos, para eso tenemos que usar Mongoose para hacer queries.

Haciendo queries

Una query (plural queries) es una consulta a una base de datos. Antes hicimos una query en la shell de Mongo.

> db.users.find()

Esa query me devuelve todos los documentos de la colección users. Con Mongoose podemos hacer lo mismo desde JavaScript usando el modelo que creamos antes. Vamos a modificar user.js (el de routes/api).

// user.js

// usamos express
const express = require('express');

// creamos un router
const router = express.Router();

// importamos el modelo User
// la ruta del archivo del modelo es relativa a la de user.js
const User = require('../../models/User');

// GET a /api/users (todos los usuarios)
router.get('/users', (req, res) => {
  User.find((err, users) => {
    if (err) throw err;
    res.status(200).json(users);
  });
});

// GET a /api/user/id (un solo usuario)
router.get('/user/:id', (req, res) => {
  User.findOne({ id: req.params.id }, (err, user) => {
    if (err) throw err;
    res.status(200).json(user);
  });
});

// hay que exportar el router para usarlo en index.js
module.exports = router;

Observen que la ruta /user/:id en realidad no es una sola sino muchas. El :id indica que ese valor es un parámetro de la ruta y todos los parámetros están disponibles en req.params.

Ahora ya no respondemos a la petición con res.send sino con res.json que toma un objeto o array de objetos y los devuelve en formato JSON. El res.status(200) es para indicar el status code de HTTP (200 significa OK).

Adelante, ejecuten el server y prueben las rutas en el navegador: http://localhost:3000/api/users o http://localhost:3000/api/user/2. Un consejo, Firefox formatea el JSON de manera más piola que Chrome, veanlo ustedes mismos.

Listo, API terminada. Pueden subir esta app a Heroku como hicimos la vez anterior. El único problema es que cuando el server esté online en Heroku no va a tener una base de datos a la cual conectarse. Para eso vamos a usar MongoDB Atlas.

Creando la base de datos (otra vez)

Vamos a crear la base de datos en MongoDB Atlas. Para eso vamos al sitio y nos logeamos. Nos recibe un dashboard en la sección de clusters. Creamos un nuevo cluster que sería algo así como un grupo de bases de datos. Nosotros vamos a tener una sola base de datos con una sola colección, pero en la vida real se complica un poco más.

Le damos al botón de crear un nuevo cluster. Nos lleva a la pantalla para elegir el tipo de servicio, elegimos la opción gratuita (free tier).

Luego nos deja elegir el proveedor y la región para nuestro cluster. Yo elegí AWS y North Virginia, pero pueden cambiar por otro.

Le dan al botón de crear cluster y los va a llevar de nuevo a la pantalla de clusters. Van a esperar unos minutos a que termine de crearse el cluster en los servidores.

Cuando termine le dan al botón de collections y luego a add my own data.

En el nombre de la base de datos pongan hellodb y en el nombre de la colección Collection0.

Antes de conectarse a la base de datos creamos un usuario de MongoDB. En el menú lateral buscamos database access y agregamos un nuevo usuario.

Le ponemos hellodb de nombre y lo demás como se muestra en la imagen, elijan un password.

Agregar IPs a la whitelist de MongoDB Atlas

Para que nuestra app de Heroku pueda conectarse a la base de datos en MongoDB Atlas necesitamos poner la IP del server de la app en una whitelist. Una whitelist es una lista de lo que permitimos acceso, en este caso serían IPs de computadoras que tienen autorizado conectarse al cluster de bases de datos que creamos en MongoDB Atlas.

Idealmente solo queremos las IPs necesarias en esa whitelist, pero como estamos usando un hosting gratuito la IP de nuestro server puede cambiar en cualquier momento. Por eso simplemente vamos a poner todas las IPs posibles, que no es lo recomendable pero tampoco tenemos opción.

En nuestro dashboard de MongoDB Atlas vamos a Network Acess y usamos el botón de agregar IP. Ahí ponemos como IP 0.0.0.0/0 que significa justamente cualquier IP. Sino hacemos esto nuestra app de Heroku nos va a dar error de conexión cuando se intente conectar a la base de datos.

Conectarse e insertar los usuarios

Volviendo a la sección de clusters le damos al botón de connect.

Como método de conexión elegimos conectar una aplicación.

Finalmente obtenemos la URI o connection string que necesitamos para nuestro código. Ojo que hay que reemplazar usuario, contraseña y nombre de la base de datos.

Vamos a nuestro script populatedb.js, comentamos la URI de la base de datos local y ponemos la que nos dió MongoDB Atlas. No se olviden de reemplazar el password (sin los <>).

// populatedb.js

// la URI de la db
const db = 'mongodb+srv://hellodb:<password>@cluster0-n6uib.mongodb.net/hellodb?retryWrites=true&w=majority';
// const db = 'mongodb://localhost/hellodb';

Volvemos a ejecutar el script pero ahora vamos a estar insertando los documentos en nuestra base de datos recién creada.

$ node populatedb.js

Nos fijamos en la web de MongoDB Atlas que los documentos estén en la base de datos, usen el botón de collections en la sección de clusters. Debería verse así.

Listo, ya tenemos una base de datos que podemos usar con Heroku. Falta decirle donde está y terminamos.

Decirle a Heroku donde está la base de datos

Doy por hecho que ya tienen la API hosteada en Heroku. Van al panel de control de su app en la web de Heroku y en la pestaña de settings hay que agregar la URI de MongoDB en la sección de config vars (variables de configuración). Recuerden reemplazar el password en la URI por el que eligieron ustedes.

Le dan al botón de agregar y hacen un deploy manual desde la pestaña de deploy. En mi caso la API está en https://hello-database.herokuapp.com/api/users o https://hello-database.herokuapp.com/api/user/1.

Agregar CORS a la API

Cuando querramos usar esta API mediante métodos AJAX (como fetch()) desde un dominio distinto al de la API nos vamos a encontrar con un error. Esto se debe a una política de seguridad de los navegadores web llamada SOP (same origin policy).

Para poder usar la API desde un frontend con distinto dominio del backend tenemos que usar CORS (cross-origin resource sharing) que simplificando un poco, no es más que setear ciertos headers en las respuestas del servidor (nuestra API). Una manera muy simple de lograr este objetivo es usar un paquete de npm llamado cors pensado para este tipo de aplicaciones en Express.

No hay que hacer demasiado, simplemente en la terminal en la carpeta de nuestro proyecto instalamos cors.

$ npm install cors

Y agregamos las siguientes líneas a index.js.

// index.js

const express  = require('express');
const mongoose = require('mongoose');

// agregamos esta linea
const cors     = require('cors');

const app = express()

// y esta otra
app.use(cors());

Con eso tenemos la API lista para la próxima guía. El middleware cors se va encargar de agregar los headers necesarios.

¿Y ahora?

Ahora tenemos tres caminos por delante, yo sugiero seguirlos en el orden en que están listados.

En esta guía utilizamos MongoDB como base de datos. En hello-postgre les muestro como hacer algo parecido con PostgreSQL y de paso usamos la CLI de Heroku para subir nuestra app.

Guías, referencias y documentación

Para los más curiosos les dejo algunos links.