Agregar una base de datos PostgreSQL a una aplicación Node.js en Heroku

Heroku es una solución generalizada para alojamiento en línea rápido y gratuito. Hay numerosos complementos que lo hacen aún más funcional. Agregaremos una base de datos Postgre a una aplicación Node.js.

Introducción

Heroku es un servicio de alojamiento que admite aplicaciones Node.js. Es fácil de usar y su funcionalidad se puede ampliar con complementos. Hay complementos para varias cosas, incluidos mensajes/colas, registro, métricas y, por supuesto, almacenes de datos. Los complementos del almacén de datos admiten bases de datos populares, como PostgreSQL, Redis y DynamoDB.

En este tutorial, agregaremos una base de datos PostgreSQL a una aplicación Node que acorta las URL. Luego implementaremos la aplicación en Heroku y configuraremos el complemento de PostgreSQL.

PostgreSQL

Si aún no lo tiene, deberá instalar Postgres en su máquina. Hay algunas formas diferentes de instalarlo, dependiendo de su sistema operativo. Visite la Página de descargas de PostgreSQL para obtener más información.

Con PostgreSQL instalado, podemos crear una base de datos para que la use la aplicación de acortador de URL:

1
2
3
4
5
$ psql
psql (11.6)
Type "help" for help.

tomkadwill=#

Y luego use el comando SQL CREATE DATABASE:

1
2
3
4
5
6
7
tomkadwill=# CREATE DATABASE urlshortener_development;
CREATE DATABASE
tomkadwill=# \l
                                         List of databases
            Name          |   Owner    | Encoding |   Collate   |    Ctype    |   Access privileges
--------------------------+------------+----------+-------------+-------------+-----------------------
 urlshortener_development | tomkadwill | UTF8     | en_US.UTF-8 | en_US.UTF-8 |

Aquí creamos una base de datos llamada urlshortener_development y luego usamos \l para imprimir una lista de todas las bases de datos PostgreSQL en el sistema.

Nuestra nueva base de datos urlshortener_development está allí, por lo que sabemos que se creó correctamente. Además, tenga en cuenta el propietario de la base de datos porque lo necesitaremos más adelante (el suyo será diferente al mío).

Integración de Postgres en una aplicación de nodo

La aplicación Node en la que trabajaremos es bastante simple. Si quieres construirlo desde cero puedes seguir nuestra guía, Implementación de una aplicación Node.js en Heroku, o puedes descargarla de GitHub.

La lógica de la aplicación Express está dentro de app.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const express = require('express');
const app = express();
const path = require('path');
const port = process.env.PORT || 3000;
const urlShortener = require('node-url-shortener');

const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({extended: true}));
app.use(express.urlencoded());

app.get('/', function(req, res) {
  res.sendFile(path.join(__dirname + '/index.html'));
});

app.post('/url', function(req, res) {
  const url = req.body.url

  urlShortener.short(url, function(err, shortUrl){
    res.send(shortUrl);
  });
});

app.listen(port, () => console.log(`url-shortener listening on port ${port}!`));

Puede ejecutar la aplicación a través de npm start. Una vez iniciado, vaya a anfitrión local: 3000 y debería ver la página de inicio:

app_homepage

El plan es actualizar app.js para que almacene cada URL y URL abreviada en una tabla de base de datos y luego muestre los últimos 5 resultados en la interfaz de usuario.

Lo primero que debemos hacer es instalar una biblioteca ORM (Object Relation Mapper). Interactuar directamente con PostgreSQL es difícil porque tendríamos que escribir nuestras propias consultas SQL sin formato.

Un ORM nos permite interactuar con la base de datos a través de llamadas API más simples. Tenga en cuenta que hay algunas desventajas en el uso de ORM, pero no las cubriré en este tutorial.

Hay varias bibliotecas ORM diferentes para Node, en este caso usaremos [Secuela] (https://github.com/sequelize/sequelize):

1
2
$ npm install --save sequelize
$ npm install --save pg pg-hstore

El primer comando instala Sequelize y el segundo instala el controlador PostgreSQL para Node. Sequelize admite varias bases de datos, por lo que debemos especificar cuál usar y proporcionar el controlador Node.

Migraciones

Con Sequelize instalado y configurado, podemos pensar en la estructura de la base de datos. Solo necesitamos algo simple, una sola tabla con 3 columnas: una ID única, una URL original y una URL abreviada.

Podríamos crear la nueva tabla de la base de datos manualmente, pero eso dificultaría las implementaciones. Tendríamos que recordar nuestras consultas y ejecutarlas en cada entorno.

Una mejor manera de manejar los cambios en la base de datos es mediante migraciones, que es donde los cambios en la base de datos se codifican dentro de la aplicación. Afortunadamente, Sequelize admite migraciones listas para usar. Escribamos una migración para crear una tabla para URL.

Primero, instalaremos Sequelize CLI, que nos permite ejecutar migraciones:

1
$ npm install --save sequelize-cli

A continuación, inicializaremos Sequelize:

1
$ npx sequelize-cli init

Esto creará un archivo config/config.json y los directorios models, migrations y seeders.

Después de eso, debemos modificar el archivo config.json para que pueda conectarse a nuestra base de datos PostgreSQL:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "development": {
    "username": "tomkadwill",
    "password": "password",
    "database": "urlshortener_development",
    "host": "localhost",
    "dialect": "postgres",
    "operatorsAliases": false
  }
}

Una vez que el archivo esté listo, generemos la migración usando la CLI de Sequelize. Aquí, definiremos nuestros campos a través de la bandera attributes. No incluiremos el campo id ya que se agrega automáticamente:

1
$ npx sequelize-cli model:generate --name Url --attributes url:string,shortUrl:string

Esto creará un archivo de migración que se parece a esto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
'use strict';
module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('Urls', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      url: {
        type: Sequelize.STRING
      },
      shortUrl: {
        type: Sequelize.STRING
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('Urls');
  }
};

Las migraciones contienen una función arriba y abajo. up se usa para mover la base de datos hacia adelante y down se usa para retroceder.

En este caso, up crea una tabla Urls que tiene 5 campos. Tiene los campos url y shortUrl, así como id, createdAt y updatedAt, que se agregan de forma predeterminada.

La migración hacia abajo simplemente eliminará la tabla Urls.

Finalmente, ejecutemos la migración:

1
$ npx sequelize-cli db:migrate

Una vez que se ejecuta, podemos consultar la base de datos directamente para verificar que todo funcionó:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ psql -p 5432 "urlshortener_development"
psql (11.6)
Type "help" for help.

urlshortener_development=# \dt
              List of relations
 Schema |     Name      | Type  |   Owner
--------+---------------+-------+------------
 public | SequelizeMeta | table | tomkadwill
 public | Urls          | table | tomkadwill
(2 rows)

urlshortener_development=# \d "Urls"
                                       Table "public.Urls"
  Column   |           Type           | Collation | Nullable |              Default
-----------+--------------------------+-----------+----------+------------------------------------
 id        | integer                  |           | not null | nextval('"Urls_id_seq"'::regclass)
 url       | character varying(255)   |           |          |
 shortUrl  | character varying(255)   |           |          |
 createdAt | timestamp with time zone |           | not null |
 updatedAt | timestamp with time zone |           | not null |
Indexes:
    "Urls_pkey" PRIMARY KEY, btree (id)

Como puede ver, hay dos tablas de base de datos: SequelizeMeta y Urls. Y si inspeccionamos Urls, los campos esperados están ahí.

Guardar URL

Ahora que tenemos configurado PostgreSQL y hemos creado una tabla de base de datos con la estructura correcta, el siguiente paso es actualizar nuestra aplicación para que persista las URL en la base de datos. Recuerde que npx sequelize-cli model:generate creó un archivo de modelo, lo usaremos para guardar las URL en la base de datos.

Primero necesitamos importar los modelos a app.js, requiriendo models/index.js:

1
const db = require('./models/index.js');

El archivo models/index.js fue generado por Sequelize y su propósito es incluir todos los archivos del modelo.

A continuación, debemos actualizar la ruta post para que cree un registro en la base de datos. Podemos usar la función findOrCreate() para que cada URL solo tenga una entrada única en la base de datos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
app.post('/url', function(req, res) {
  const url = req.body.url

  urlShortener.short(url, function(err, shortUrl) {
    db.Url.findOrCreate({where: {url: url, shortUrl: shortUrl}})
    .then(([urlObj, created]) => {
      res.send(shortUrl)
    });
  });
});

Cuando se llama a db.Url.findOrCreate(), intentará encontrar un registro que coincida con la url proporcionada por el usuario y la shortUrl generada. Si se encuentra uno, Sequelize no hace nada, de lo contrario, crea un nuevo registro.

Visualización de direcciones URL en la interfaz de usuario

Para facilitar el uso, actualicemos la aplicación para mostrar las últimas 5 URL persistentes.

Con ese fin, agregaremos un motor de plantillas para que Express pueda representar las URL dinámicamente. Hay muchos motores de plantillas disponibles, pero en este caso usaremos Manillar Express:

1
$ npm install --save express-handlebars

Después de instalar el paquete, podemos agregarlo a app.js:

1
2
3
4
const exphbs = require('express-handlebars');

app.engine('handlebars', exphbs());
app.set('view engine', 'handlebars');

A continuación, debemos cambiar la estructura del directorio de la aplicación. express-handlebars asume que las vistas están ubicadas en un directorio views. También asume que tenemos un archivo views/layouts/main.handlebars:

1
2
3
4
5
6
.
├── app.js
└── views
    ├── index.handlebars
    └── layouts
        └── main.handlebars

Entonces, movámonos y cambiemos el nombre del archivo index.html:

1
$ mv index.html views/index.handlebars

Y finalmente, hagamos un archivo de diseño, views/layouts/main.handlebars:

1
2
3
4
5
6
7
8
<html>
<head>
    <title>Url Shortener</title>
</head>
<body>
    {{{body}}}
</body>
</html>

Hay un cierto orden en el que se cargan los archivos de plantilla: express-handlebars generará views/layouts/main.handlebars que luego representa views/index.handlebars dentro del {{{body}}} etiqueta.

Ahora que tenemos la estructura de directorios correcta, agreguemos algo de código HTML a index.handlebars para mostrar dinámicamente las URL:

1
2
3
4
5
<ul>
  {{#each urlObjs}}
  <li>{{this.url}} -- <b>{{this.shortUrl}}</b></li>
  {{/each}}
</ul>

Aquí usamos Handlebars' each helper para iterar sobre cada objeto URL y mostrar la URL original y la URL corta.

Lo último que debemos hacer es actualizar la ruta GET en app.js para pasar las URL a la vista:

1
2
3
4
5
6
7
8
app.get('/', function(req, res) {
  db.Url.findAll({order: [['createdAt', 'DESC']], limit: 5})
  .then(urlObjs => {
    res.render('index', {
      urlObjs: urlObjs
    });
  });
});

Repasemos lo que está pasando aquí. Cuando se solicita /, Sequelize consulta la tabla Urls. La consulta está ordenada por createdAt y limitada a 5, lo que garantiza que solo se devuelvan los 5 resultados más recientes.

El resultado de la consulta se pasa a res.render como la variable local urlObjs, que será utilizada por la plantilla.

La página renderizada se ve así:

app_homepage_with_db

Si tiene algún problema, puede descargar el código completo desde [GitHub](https://github.com/tomkadwill/url-shortener/tree/adding-a-postgres-database-to-a-node-app-on -heroku).

Implementación de la aplicación en Heroku {#implementación de la aplicación en Heroku}

Ahora que tenemos la aplicación ejecutándose localmente, esta sección cubrirá cómo ejecutarla en Heroku y cómo conectar la base de datos una vez que se esté ejecutando.

Primero, implementemos todos nuestros cambios en Heroku:

1
$ git push heroku master

See Implementación de una aplicación Node.js en Heroku for a detailed guide on deploying to Heroku.

Conexión de la base de datos {#conexión de la base de datos}

Agregar una base de datos no es difícil, y todo lo que requiere es una sola línea de comando:

1
$ heroku addons:create heroku-postgresql:hobby-dev

Este comando crea el complemento PostgreSQL para Heroku y establece una variable de entorno llamada DATABASE_URL; solo necesitamos decirle a Sequelize que lo use actualizando el archivo de configuración:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
  "development": {
    "username": "tomkadwill",
    "password": "password",
    "database": "urlshortener_development",
    "host": "localhost",
    "dialect": "postgres",
    "operatorsAliases": false
  },
  "production": {
    "username": "tomkadwill",
    "password": "password",
    "database": "urlshortener_production",
    "host": "localhost",
    "dialect": "postgres",
    "operatorsAliases": false,
    "use_env_variable": "DATABASE_URL"
  }
}

Aquí hemos agregado una nueva sección de configuración para la producción, es lo mismo que el desarrollo excepto por el nombre de la base de datos y el campo use_env_variable.

Sequelize usa use_env_variable para obtener el nombre de la variable de entorno que se usará para conectarse a la base de datos.

Heroku tiene NODE_ENV establecido en "producción" de forma predeterminada, lo que significa que buscará las configuraciones de producción. Comprometámonos y empujemos a Heroku.

A continuación, necesitamos ejecutar las migraciones en Heroku usando sequelize db:migrate:

1
2
3
$ heroku run bash
Running bash on  nameful-wolf-12818... up, run.5074 (Free)
~ $ sequelize db:migrate

Si no hemos creado la migración antes, ahora estaríamos ejecutando scripts manualmente.

En este punto, la aplicación debería estar funcionando en el navegador.

app_homepage_with_db_on_heroku

Gestión de la base de datos de Heroku

Probablemente necesitará consultar o modificar su base de datos de producción en algún momento. Por ejemplo, es posible que deba consultar algunos datos para ayudar a diagnosticar un error o que deba modificar algunos datos de usuario. Veamos cómo hacerlo.

Ejecución de tareas de migración de Sequelize

Lo primero que debe saber es cómo ejecutar las migraciones de Sequelize en Heroku. Esta es la forma de ver una lista de los comandos disponibles:

1
2
3
$ heroku run bash
Running bash on  nameful-wolf-12818... up, run.1435 (Free)
~ $ sequelize

Estos son algunos de los más útiles:

  • sequelize db:migrate:undo: Deshacer una sola migración
  • sequelize db:migrate:undo:all: Deshacer todas las migraciones. Revertir efectivamente a una base de datos limpia
  • sequelize db:migrate:status: compruebe en qué migración se encuentra su aplicación
Uso de Sequelize Models en la consola

Además de las tareas de CLI, podemos usar modelos de Sequelize directamente en la consola de Node:

1
2
3
4
5
$ heroku run bash
~ $ node
Welcome to Node.js v12.14.0.
Type ".help" for more information.
>

Estos son algunos ejemplos de código que se pueden ejecutar en la consola de Node. Puede notar que se parecen al Nodo REPL.

Consulta de una URL individual por ID:

1
2
3
4
5
db.Url.findByPk(1).then(url => {
  console.log(
    url.get({plain: true})
  );
});

Consultando todas las URL:

1
2
3
4
5
6
7
db.Url.findAll().then(urls => {
  urls.map(url => {
    console.log(
      url.get({plain: true})
    );
  });
});

Insertar un registro de URL:

1
db.Url.create({url: 'https://wikihtp.com/deploying-a-node-js-app-to-heroku', shortUrl: 'https://is.gd/56bEH3'});

Consultar una URL por ID y actualizarla:

1
2
3
4
db.Url.findByPk(1).then(url => {
  url.shortUrl = 'example.com';
  url.save();
});

Consultar una URL por ID y eliminarla:

1
2
3
db.Url.findByPk(1).then(url => {
  url.destroy();
});

Conclusión

Hay numerosos complementos que se pueden usar para extender Heroku. Uno de esos complementos es Heroku Postgres, que le permite configurar fácilmente una base de datos para almacenar datos de aplicaciones.

Hemos ampliado una aplicación simple de Node y Express para que almacene direcciones URL en una base de datos de Postgres, en Heroku.