Prerequisites
This guide is an evolution of what I have already described in the guide that follows: https://dev.to/jardiin/connecting-a-serverless-postgresql-database-neon-to-nestjs-using-the-config-service-221l
Introduction
The main purpose of this project is to show you how to define multiple instances of configuration files in NestJS, using the cross-env library and creating the necessary number of .env files. This allows you to:
- Use different database instances for development, staging, or production environments.
- Clean up and maintain .env files.
- Establish a solid structure for the config/ folder.
Project Structure
This is the structure of my project. Essentially, we will be touching files located within the config folder, the app.module.ts file, and the main.ts file. The structure of the other folders (such as controller, entities, modules, services) may vary compared to yours because all my files (e.g., controllers) are saved within the controllers/ folder and exported from the index file.
├── src
│ ├── config
│ │ ├── development.config.ts
│ │ ├── production.config.ts
│ │ ├── app.config.ts
│ │ ├── typeorm.config.ts
│ │ └── index.ts
│ │ └── other config file...
│ ├── controllers
│ │ └── index.ts
│ │ └── users.controller.ts
│ ├── entities
│ │ └── index.ts
│ │ └── users.entity.ts
│ ├── modules
│ │ └── index.ts
│ │ └── users.module.ts
│ ├── services
│ │ └── index.ts
│ │ └── users.service.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ └── main.ts
├── package.json
├── tsconfig.json
├── other nestjs files..
├── .env.production
└── .env.development
tsconfig.json
This is the file that contains the compilerOptions of the project in Nest, to which I have applied the modifications that follow for the custom exporters I mentioned above (of various index.ts files).
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "src",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"paths": {
"@app/*": ["app/*"],
"@controllers/*": ["controllers/*"],
"@entities/*": ["entities/*"],
"@modules/*": ["modules/*"],
"@services/*": ["services/*"],
"@config/*": ["config/*"],
... _OTHER_CUSTOM_PATH_HERE_ ...
}
}
}
(1) Package.json - Scripts Update
At this point, it becomes necessary to update the various scripts within the package. In this way, thanks to the cross-env library, we will be able to define the development/prod environment of NODE_ENV.
"scripts": {
"build": "nest build",
"start:production": "cross-env NODE_ENV=production nest start --watch",
"start:development": "cross-env NODE_ENV=development nest start --watch",
... OTHER_SCRIPTS_HERE ...
},
It is recommended to define the NODE_ENV within the scripts, giving the same naming convention as the .env files!
(2) Environment Files
In these files the environment variables for linking to the various databases will be defined. In my case the connection is made to Neon PostgreSQL databases. The .env files should look similar:
.env.development / .env.production
# ====== NEON POSTGRES ====== #
DATABASE_HOST='NEON PGHOST'
DATABASE_USER='NEON PGUSER'
DATABASE_PASSWORD='NEON PGPASSWORD'
DATABASE_PORT= 5432 #(usually it's 5432)
DATABASE_ENDPOINT_ID='NEON ENDPOINT_ID'
DATABASE_NAME='NEON PGDATABASE'
(3) Config Files
Considering that point (1) has been completed with the installation of the necessary dependencies (I remind you again that additional dependencies were installed in the guide mentioned in the prerequisites), it will now be possible to define the various configuration files.
The following files configure the connection to PostgreSQL databases, designed for development and production, using the credentials defined in the respective .env files.
(3.1) development.config.ts
import * as dotenv from 'dotenv';
dotenv.config({
path: `.env.development`,
});
import { registerAs } from '@nestjs/config';
export default registerAs('development', () => ({
type: 'postgres', // Neon PostgreSQL database type
host: process.env.DATABASE_HOST || 'localhost',
database: process.env.DATABASE_NAME,
port: process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
entities: [`${__dirname}/../**/*.entity{.ts,.js}`], // TypeORM Entities to be stored in the database
subscribers: [`${__dirname}/../**/*.subscriber{.ts,.js}`], // OPTIONAL
synchronize: process.env.NODE_ENV === 'development', // Set `true` to synchronize the database schema with the entities
logging: true,
ssl: true,
connection: {
options: `project=${process.env.DATABASE_ENDPOINT_ID}`,
},
migrations: [`${__dirname}/../database/migrations/*{.ts,.js}`], // Migrations
migrationsTableName: 'typeorm-migrations', // Set the name of the migrations table
}))
(3.2) production.config.ts
import * as dotenv from 'dotenv';
dotenv.config({
path: `.env.production`,
});
import { registerAs } from '@nestjs/config';
export default registerAs('production', () => ({
type: 'postgres', // Neon PostgreSQL database type
host: process.env.DATABASE_HOST || 'localhost',
database: process.env.DATABASE_NAME,
port: process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
entities: [`${__dirname}/../**/*.entity{.ts,.js}`], // TypeORM Entities to be stored in the database
subscribers: [`${__dirname}/../**/*.subscriber{.ts,.js}`], // OPTIONAL
synchronize: process.env.NODE_ENV === 'development', // Set `true` to synchronize the database schema with the entities
logging: false,
ssl: true,
connection: {
options: `project=${process.env.DATABASE_ENDPOINT_ID}`,
},
migrations: [`${__dirname}/../database/migrations/*{.ts,.js}`], // Migrations
migrationsTableName: 'typeorm-migrations', // Set the name of the migrations table
}))
(3.3) app.config.ts
This setup register the application configuration settings under the name 'config' with @nestjs/config. Here we will define the port and node environment.
import { registerAs } from '@nestjs/config';
export default registerAs('config', () => ({
port: 5575,
nodenv: process.env.NODE_ENV,
}));
(3.4) typeorm.config.ts
Inside this folder we configure a TypeORM DataSource for Neon database using settings from environment variables.
// ====== IMPORTS =========
import { DataSource } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import { config } from 'dotenv';
config();
// 1. Define a configuration for TypeORM
const configService = new ConfigService();
export default new DataSource({
type: 'postgres', // Neon PostgreSQL database type
host: configService.get<string>('DATABASE_HOST'),
database: configService.get<string>('DATABASE_NAME'),
port: configService.get<number>('DATABASE_PORT'),
username: configService.get<string>('DATABASE_USER'),
password: configService.get<string>('DATABASE_PASSWORD'),
entities: [`${__dirname}/../src/**/*.entity{.ts,.js}`],
subscribers: [`${__dirname}/../**/*.subscriber{.ts,.js}`] // OPTIONAL,
synchronize: process.env.NODE_ENV === 'development',
logging: process.env.NODE_ENV === 'development',
ssl: true,
migrations: [`${__dirname}/../database/migrations/*{.ts,.js}`],
migrationsTableName: 'typeorm-migrations', // Set the same name as you did on database.config
})
(3.5) config/index.ts
Now it is finally possible to export, within the index file the files you just created
export { default as DevelopmentConfig } from './development.config';
export { default as ProductionConfig } from './production.config';
export { default as AppConfig } from './app.config';
(4) App Files
The app.module.ts
orchestrates the main structure and dependencies of the NestJS application. It imports the necessary modules to configures TypeORM for database access using environment variables. In questo caso eseguiremo un aggiornamento del ConfigModule e del TypeORM module come segue :
// ======== MODULES =========
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
// ======== CONTROLLERS =========
import { AppController } from './app.controller';
// ======== SERVICES =========
import { AppService } from './app.service';
// ======== CONFIG =========
import { AppConfig, DevelopmentConfig, ProductionConfig } from 'config/index';
@Module({
imports: [
// Load environment variables
ConfigModule.forRoot({
isGlobal: true, // Set `true` for global configuration
cache: true,
load: [AppConfig, DevelopmentConfig, ProductionConfig],
}),
// TypeORM configuration
TypeOrmModule.forRootAsync({
imports: [ConfigModule], // Import the ConfigModule
useFactory: (configService: ConfigService) => ({
...configService.get(
process.env.NODE_ENV === 'production' ? 'production' : 'development',
),
}),
inject: [ConfigService], // Inject the ConfigService
}),
// Other Project Modules
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }
The condition process.env.NODE_ENV === 'production' ? 'production' : 'development' is used to determine which configuration options to use based on the value of the NODE_ENV environment variable.
Here's what it does:
- If NODE_ENV is set to 'production', it returns the configuration options for the production environment.
- If NODE_ENV is set to anything else (including undefined), it returns the configuration options for the development environment.
Conclusion
That's it! You will now be able to run via terminal the commands necessary to connect to the database of your choice, whether it is intended for production or development, via the following commands :
yarn start:production
yarn start:development
If all goes well, you will have from the terminal a message like this for your Database.
Additional Resources
- Managing Multiple environments in NestJS: https://dev.to/pitops/managing-multiple-environments-in-nestjs-71l
- Setting Up Environment Variables and Configurations in NestJS: https://bhargavacharyb.medium.com/setting-up-environment-variables-and-configurations-in-nestjs-a6372fb81f31
Top comments (3)
Node (recent versions) includes built-in environment file selection in the CLI and programmatically. In other words
dotenv
is no longer needed.See here
This is a very thorough guide! I appreciate the step-by-step explanation of setting up multiple environments with dotenv and database configurations in NestJS. I'm looking forward to implementing this in my own project.
Thank you very much!!! In the future I would also like to provide zipped sourcecode or an IDE like Stackblitz to use as a reference!