Diferentes configuraciones y entornos en node

Una de mis obsesiones cuando empiezo un proyecto, sobre todo si el lenguaje de programación o el framework que estoy usando es relativamente nuevo para mí es cómo organizar el proyecto. En este aspecto básicamente siempre me surjen varias cuestiones a tener en cuenta:
- ¿Cómo organizo el código para facilitar la puesta en producción?
- ¿Cómo lo están organizando otros desarrolladores? ¿Hay una estructura estándar?
- ¿Cómo gestiono las diferentes configuraciones en los diferentes entornos?
Hoy voy a centrarme en esta última cuestión.
Definir variables de entorno
Normalmente necesitaremos diferentes configuraciones para los diferentes entornos en los que puede funcionar nuestra aplicación, máquina de desarrollo, entorno de test, staging, producción…
Teniendo en cuenta que no es buena idea guardar algunos datos privados como las contraseñas de acceso a la base de datos, lo que resulta más sencillo es guardar las configuraciones en variables de entorno y cargarlas desde un archivo de configuración. De esta forma no tenemos que preocuparnos de modificar el código en ninguno de los casos y el mismo código nos sirve para los diferentes entornos.
En node podemos acceder a las variables de entorno utilizando el objeto process.env
, de forma que, por ejemplo, podemos declarar una variable de entorno DB_NAME
que contenga el nombre de nuestra base de datos y acceder a ella desde el código simplente:
const dbName = process.env.DB_NAME
Valor por defecto
Si lo que queremos es utilizar un valor por defecto, lo que nos puede facilitar el desarrollo en un momento dado, bastará con utilizar el operador «|» para que si la variable de entorno no está definida se use dicho valor. Por ejemplo:
const dbName = process.env.DB_NAME | 'mi_base_de_datos'
Comprobar variables obligatorias
Una alternativa a establecer valores por defecto es obligar al entorno a que defina algunos valores y lanzar una excepción en caso de que no estén definidos, antes de ejecutar nuestra aplicación. Para esto podemos, por ejemplo, definir un array con las variables de entorno que obligatoriamente deben estar establecidas y recorrerla lanzando un error en caso de que alguna de ellas no lo esté. Por ejemplo:
const requiredEnvVars = [ 'DB_NAME', 'DB_PASSWD' ]
requiredEnvVars.forEach((name) => {
if (!process.env[name]) {
throw new Error(`Debes definir la variable de entorno ${name}`);
}
});
Un módulo de configuraciones
Por último, resulta bastante cómodo definir un archivo config.js
que mantenga todas las configuraciones en un solo objeto bien ordenado y que exporte dicho objeto para que pueda ser importado fácilmente desde cualquier módulo de la aplicación que pueda necesitarlos.
const config = {
db: {
name: process.env.DB_NAME,
password: process.env.DB_PASSWD
}
}
module.exports = config
Uniéndolo todo
Si unimos todos los conceptos anteriores, la idea final sería crear un archivo config.js
que comprobara la existencia de las variables de entorno con los valores obligatorios, los valores opcionales tendrían un valor por defecto establecido y todo ello estaría contenido en un sólo objeto que finalmente sería exportado. Un archivo de configuración de ejemplo podría ser algo como esto:
// config/config.js
'use strict'
// comprobación de variables obligatorias
const requiredEnvVars = [ 'AMQP_URL', 'DB_USER', 'DB_PASSWD' ]
requiredEnvVars.forEach((name) => {
if (!process.env[name]) {
throw new Error(`Debes definir la variable de entorno ${name}`);
}
});
// definición del objeto de configuraciones
const config = {
database: {
host: process.env.DB_HOST | '127.0.0.1',
port: process.env.DB_PORT | '27017',
user: process.env.DB_USER,
password: process.env.DB_PASSWD,
name: process.env.DB_NAME | 'mi_base_de_datos'
},
env: process.env.NODE_ENV | 'debug',
logger: {
level: process.env.LOG_LEVEL | 'warn',
enabled: process.env.LOG_ENABLED ? process.env.LOG_ENABLED.toLowerCase() === 'true' : false
},
queue: {
url: process.env.AMQP_URL
},
server: {
port: process.env.PORT ? Number(process.env.PORT) : 8000
}
}
module.exports = config
Algo a tener en cuenta es que todos los valores leídos desde las variables de entorno son cadenas de texto y, por tanto, hay que convertirlas al tipo de datos necesario antes de utilizarlas. Siempre es mejor realizar este paso tras la lectura, para, en caso de error, detectarlo en el momento del arranque.
Conclusión
Para la mayoría de las aplicaciones este esquema es más que suficiente y bastante cómodo de utilizar, pero, como siempre, no hay un caso que sirva para todo. En ocasiones puede resultar de utilidad, por ejemplo, dividir el archivo de configuración según los componentes o módulos de nuestra aplicación. En este caso, la política a seguir en cada uno de ellos sería la misma pero a lo largo de múltiples archivos, que guardaremos siempre en el directorio config
. Por ejemplo:
...
|-- config
| |-- common.js
| |-- i18n.js
| |-- logger.js
| `-- rabbitmq.js
...
Espero que te haya resultado útil.
Te espero en el próximo artículo.
Stay tuned and happy coding!