Una vez creado tu fork y clonado el repo en tu computadora, antes de poder comenzar a codear, tenemos primero que crear nuestro entorno de desarrollo. Para ello te recomendamos seguir los pasos a continuación:
- 1. Elegir base de datos
- 2. Instalar
docker
ydocker-compose
- 3. Configurar "servicio" de base de datos
- 4. Configurar conexión a BBDD en "servicio" node
- 5. Elegir módulo (cliente)
- 6. Iniciar, re-iniciar y parar los servicios con
docker-compose
- 7. Familiarizarte con admisitración de contenedores
- 8. Opcionalmente, instalar interfaz gráfica para admisitrar data
- 9. Definir esquemas
- 10. Definir estrategia de pruebas unitarias
- 11. Familiarizarte con las pruebas de integración (e2e)
La primera decisión que tenemos que tomar, antes de comenzar a programar, es elegir una base de datos. En este proyecto se sugieren 3 opciones: dos de ellas relacionales y basadas en SQL, (PostgreSQL y MySQL), y otra no relacional (MongoDB). Las 3 son excelentes opciones.
Algunos puntos a tener en cuenta:
- MongoDB es la más común (popular) a día de hoy en el ecosistema de Node.js.
- Las bases de datos relacionales normalmente requieren más diseño a priori (definir tablas, columnas, relaciones, ...) mientras que las no relacionales nos permiten ser más flexibles.
- Las bases de datos relacionales nos permiten relacionar datos de forma más natural y garantizar la consistencia de la data. Nos dan una rigidez que quita flexibilidad pero agrega otro tipo de garantías, además de permitirnos pensar en tablas y columnas, que es una idea con la que muchas ya están familiarizadas.
- MySQL, PostgreSQL y MongoDB (en ese orden) son las bases de datos de código abierto (Open Source) más populares a diciembre de 2020. Esto en el panorama general de las bases de datos, no solo el ecosistema de Node.js.
- PostgreSQL es una base datos objeto-relacional (ORDBMS), mientras que MySQL es puramente relacional. PostgreSQL tiene soporte nativo para objetos JSON y otras características como indización de JSON.
Independientemente de qué base datos elijas, en este proyecto vamos a ejecutar
localmente (en nuestra computadora) el servidor de bases de datos usando
contenedores de Docker en vez de instalar el programa directamente en nuestra
computadora. Además vamos a usar también la herramienta docker-compose
para
orquestar nuestros contenedores: bases de datos y servidor web (node).
En los siguientes links puedes ver cómo instalar docker
y docker-compose
en
tu sistema opetativo.
El boilerplate de este proyecto incluye un archivo
docker-compose.yml
que ya contiene parte de la
configuración de docker-compose
. En este archivo veremos que hay listados 2
servicios: db
y node
. Nuestra aplicación va a consistir de dos servidores:
un servidor de bases de datos (el servicio db
) y un servidor web implementado
en Node.js (el servicio node
).
En la sección correspondiente al servicio db
hay 3 cosas importantes que
tendremos que completar:
- Qué imagen (
image
) queremos usar. Imágenes recomendadas: mongo, postgres y mysql. - Qué volúmenes (
volumes
), archivos o carpetas, queremos mapear al contenedor, como por ejemplo el directorio de datos (la carpeta donde la base de datos guardará sus archivos). - Las variables de entorno (
environment
) necesarias para configurar nuestra base de datos y usuarios. Estos datos nos van a servir después para configurar la conexión desde Node.js.
Ejemplo de servicio db
usando MongoDB:
db:
image: mongo:4
volumes:
- ./db-data:/data/db
environment:
MONGO_INITDB_ROOT_USERNAME: bq
MONGO_INITDB_ROOT_PASSWORD: secret
restart: always
networks:
- private
Ejemplo de servicio db
usando PostgreSQL:
db:
image: postgres:13
volumes:
- ./db-data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: secret
restart: always
networks:
- private
Ejemplo de servicio db
usando MySQL:
db:
image: mysql:5
volumes:
- ./db-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: supersecret
MYSQL_DATABASE: bq
MYSQL_USER: bq
MYSQL_PASSWORD: secret
restart: always
networks:
- private
Ahora que ya tenemos la configuración del servicio db
, tenemos que
completar la configuración del servicio de Node.js. En particular nos
interesa establecer el valor de la variable de entorno DB_URL
, donde
tendremos que poner el connection string correspondiente a nuestra base de
datos. Este string de conexión sigue el formato de URL y se ve así:
protocol://username:password@host:port/dbname?opt1=val1&...
Acá sustituiremos protocol
con el nombre del protocolo de la base de datos
elegida (mongodb
, postgresql
o mysql
) y username
, password
y
dbname
con los valores usados en la configuración del servicio db
en el
punto anterior. En este caso el valor de host
será db
, que es el nombre
del servicio de base de datos en la configuración de docker-compose.yml
y
podemos referirnos a él por su nombre en la red interna entre los
contenedores. Siguiendo con los ejemplos del punto anterior, la variable
DB_URL
en docker-compose.yml
se vería así:
-
MongoDB:
DB_URL: mongodb://bq:secret@db:27017/bq?authSource=admin
-
PostgreSQL:
DB_URL: postgresql://postgres:secret@db:5432/postgres?schema=public
-
MySQL:
DB_URL: mysql://bq:secret@db:3306/bq
Ahora que ya tenemos un servidor de bases de datos vamos a necesitar elegir un módulo o librería diseñado para interactuar con nuestra base de datos desde Node.js. Existen un montón de opciones, pero para este proyecto te recomendamos elegir una de estas (que son las más populares para cada una de las bases de datos): Mongoose (MongoDB), pg (PostgreSQL) o mysql (MySQL).
El boilerplate ya incluye un archivo config.js
donde se leen las
variables de entorno, y entre ellas está DB_URL
. Como vemos ese valor lo
estamos asignando en la propiedad dbUrl
del módulo config
.
// `config.js`
exports.dbUrl = process.env.DB_URL || "mongodb://localhost:27017/test";
Ahora que ya sabemos dónde encontrar el connection string (en el módulo config), podemos proceder a establecer una conexión con la base de datos usando el cliente elegido.
Ejemplo de conexión usando Mongoose (MongoDB):
const mongoose = require("mongoose");
const config = require("./config");
mongoose
.connect(config.dbUrl, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(console.log)
.catch(console.error);
Ejemplo de conexión usando pg (PostgreSQL):
const pg = require("pg");
const config = require("./config");
const pgClient = new pg.Client({ connectionString: config.dbUrl });
pgClient.connect();
pgClient.query("SELECT NOW()", (err, res) => {
console.log(err, res);
pgClient.end();
});
Ejemplo de conexión usando mysql (MySQL):
const mysql = require("mysql");
const config = require("./config");
const connection = mysql.createConnection(config.dbUrl);
connection.connect();
connection.query("SELECT 1 + 1 AS solution", (error, results) => {
if (error) {
return console.error(error);
}
console.log(`The solution is: ${results[0].solution}`);
});
connection.end();
Ahora que ya tenemos nuestra configuración de docker-compose
lista, veamos
cómo podemos levantar la aplicación. Para eso usamos el comando
docker-compose up
dentro de la carpeta de nuestro proyecto (donde está el
archivo docker-compose.yml
).
docker-compose up
Para interrumpir el comando y que el terminal nos devuelva el prompt, usa
Ctrl + C
.
Si usamos el comando así, sin opciones, esto levantará todos los servicios
descritos en el docker-compose.yml
. Alternativamente podemos arrancar un
servicio en particular agregando el nombre del servicio en el comando. Por
ejemplo, si queremos levantar sólo la base de datos:
docker-compose up db
También tenemos la opción de arrancar los servicios y que corran en el fondo,
como daemons, con la opción -d
, de forma que inmediatamente se nos devuelve
el prompt y los servicios quedan corriendo.
docker-compose up -d
Además del comando docker-compose up
, que construye, (re)crea, arranca, y
se conecta a los contenedores de unos servicios, también tenemos disponibles
comandos para iniciar (start
), reiniciar (restart
) y parar (stop
)
servicios con contenedores ya existentes.
docker-compose start
docker-compose stop
docker-compose restart
Al igual que con docker-compose up
, con estos otros comandos también podemos
especificar con qué servicio queremos interactuar (o con todos en caso de no
especificarlo). Por ejemplo, para inicar, reiniciar y después parar el servidor
de bases de datos:
docker-compose start db
docker-compose stop db
docker-compose restart db
Además de los comandos que ya hemos visto de docker-compose
, te recomendamos
familiarizarte con estos otros comandos (entre otros) para poder administrar
tus contenedores.
El comando docker-compose ps
nos muestra un listado con los contenedores
activos:
docker-compose ps
También podemos listar todos los contenedores, incluyendo los que están
detenidos usando la opción -a
:
docker-compose ps -a
Para borrar los contenedores de los servicios:
docker-compose rm
Al igual que con los comandos anteriores, también podemos borrar los contenedores de un servicio en particular indicando su nombre:
docker-compose rm db
Finalmente, cuando corremos nuestros servicios en el fondo, como daemons, para conectarnos a los contenedores y ver los logs podemos usar:
docker-compose logs
Podemos agregar también la opción -f
para hacer streaming de los logs y
quedarnos escuchando, así como especificar un servicio en particular. Por
ejemplo:
docker-compose logs -f db
Recuerda que siempre puedes consultar la ayuda de docker-compose
con el
sucomando help
. Por ejemplo, si queremos ver la ayuda del subcomando up
,
podríamos hacer esto:
docker-compose help up
A la hora de trabajar con bases de datos es muy común usar algún tipo de interfaz gráfica que nos permita ver y manipular visualmente nuestra data. Hay opciones para cada base de datos. Recomendamos las siguientes: Compass (MongoDB), Workbench (MySQL), pgAdmin (PostgreSQL).
Si quieres usar este tipo de herramientas (como Compass
o WorkBench
), es
probable que tengas que hacer que tu base de datos sea visible fuera de
docker. Para ello puedes mapear el puerto de la base datos en el contenedor a
algún puerto que esté libre en el host de docker (normalmente tu
computadora). Normalmente vamos a mapear estos puertos estándar (por ejemplo
27017
para MongoDB) a otros números de puerto distintos ya que estos
programas y/o sus puertos ya podrían estar en uso. Por ejemplo, si usamos
MongoDB, podríamos agregar el siguiente mapeo de puertos al servico db
en
nuestro docker-compose.yml
:
ports:
- 28017:27017
Al listar los puertos de un contenedor o servicio en docker-compose.yml
ten
en cuenta que el número de la derecha es el puerto en el contenedor (red
privada de docker), mientras que el número de la izquierda es el puerto en
host de docker (normalmente nuestra computadora - 127.0.0.1
o localhost
).
En el ejemplo de arriba estamos mapeando el puerto 27017
del contenedor
al puerto 28017
del host de docker.
Si estás usando PostgreSQL o MySQL, los puertos que nos interesaría mapear
serían el 5432
y 3306
respectivamente.
Si estamos exponiendo el puerto en nuestra computadora (el host), además
tendrás también que conectar el contenedor db
a la red pública:
networks:
- public
- private
Después de este cambio podrás acceder usando 127.0.0.1
o localhost
y el
puerto al que hemos mapeado, 28017
en este ejemplo.
Si eliges pgAdmin (PostgreSQL), la opción más
fácil es usar pgAdmin como contenedor y agregarlo como un nuevo servicio a
nuestro docker-compose.yml
. Por ejemplo:
pgadmin:
image: dpage/pgadmin4
restart: always
environment:
PGADMIN_DEFAULT_EMAIL: [email protected]
PGADMIN_DEFAULT_PASSWORD: secret
ports:
- 8088:80
networks:
- public
- private
NOTA: Para conectar desde pgAdmin usando un contenedor, usa el nombre del
contenedor de la base datos (ie: XXX-001-burger-queen-api_db_1
) como nombre
de host para que pgAdmin se pueda conectar a través de la red privada.
Llegado a este punto ya deberíamos tener una configuración de docker-compose
capaz de levantar la base datos y servidor de Node.js.
Como parte del proceso de diseño de nuestra base de datos vamos a tener que especificar los esquemas de nuestros modelos de datos. Con esto nos referimos a que tenemos que describir de alguna forma las colecciones o tablas que vamos a usar y la forma de los objetos o filas que vayamos a guardar en esas colecciones.
Si has elegido MongoDB y Mongoose, este último nos ofrece un mecanismo para describir esos modelos y esquemas de datos en JavaScript.
Si has elegido usar una base de datos SQL, es común incluir algunos scripts
.sql
con el código SQL que nos permita crear (o alterar) las tablas
necesarias. Alternativamente, podrías también explorar abstracciones más
modernas como Prisma.
Además de las pruebas e2e
que ya incluye el boilerplate del proyecto, se
espera que puedas también usar pruebas unitarias para el desarrollo de los
diferentes endpoints o rutas así como otros módulos internos que decidas
desarrollar.
Para hacer pruebas unitarias de rutas de Express, te recomendamos explorar la
librería supertest
, que puedes usar
en combinación con jest
.
Otro punto a tener en cuenta en las pruebas unitarias, es cómo mockear la base
de datos. Algunas bases de datos ofrecen herramientas (como
mongodb-memory-server
) que
nos permiten usar una base de datos en memoria y así evitar hacer mocks per
se, pero por lo general querremos considerar cómo abstraer la interacción
con la base de datos para facilitar mocks que nos permitan concentrarnos en
la lógica de las rutas.
El boilerplate de este proyecto ya incluye pruebas e2e
(end-to-end) o de
integración, que se encargan de probar nuestra aplicación en conjunto,
através de la interfaz HTTP. A diferencia de las pruebas unitarias, en vez
de importar o requerir un módulo y probar una función de forma aislada, lo que
vamos a hacer es arrancar toda la aplicación, y probarla tal como se usaría en
el mundo real, para ello la aplicación de prueba necesitará una base de datos y
escuchar en un puerto de red.
Para correr pruebas e2e sobre instancia local podemos usar:
npm run test:e2e
Esto levanta la aplicación con npm start
y corre los tests contra la URL de
esta instancia (por defecto http://127.0.0.1:8080
). Esto asume que la base de
datos está disponible.
Alternativamente, y quizás más fácil de usar, podemos también levantar nuestra
aplicación usando docker-compose
, o incluso en producción, y después correr
las pruebas e2e
pasando la URL de la aplicación en la variable de entorno
REMOTE_URL
. Por ejemplo:
REMOTE_URL=http://127.0.0.1:8080 npm run test:e2e
Al especificar REMOTE_URL
, los tests no tratarán de levantar un servidor
local sino que usarán directamente la URL provista asumiendo que la aplicación
está disponible en dicha URL. Esto nos permite probar también contra URLs
remotas. Por ejemplo:
REMOTE_URL=https://api.my-super-app.com npm run test:e2e