Elegant environment handling in Nest.js - node.js

I'm exploring using Nest.js for a critical application that currently has very little test-coverage. We need to make decisions based on environment flags, mostly loading additional express middleware, different loggin configuration etc. I'm using the approach to environment variables as described in the documentation, but am a bit unsure of how to elegantly (isolated, testable) handle further branching. I could handle all of this in my root module's configure hook, but feel like it'd get messy, even if I isolate it into individual methods, and there might be a better solution out there. Any help would be greatly appreciated! Thanks! ✌️

This is how I solved when configuring the project and also an example of mongoose connection
config/config.module.ts
import { Module } from '#nestjs/common';
import { ConfigService } from './config.service';
#Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}
As the .env file will not be used for production
config/config.service.ts
import * as dotenv from 'dotenv';
import * as fs from 'fs';
export class ConfigService {
MONGODB_URI: string;
private readonly envConfig: { [key: string]: string };
constructor() {
if (
process.env.NODE_ENV === 'production' ||
process.env.NODE_ENV === 'staging'
) {
this.envConfig = {
MONGODB_URI: process.env.MONGODB_URI,
};
} else {
this.envConfig = dotenv.parse(fs.readFileSync('.env'));
}
}
get(key: string): string {
return this.envConfig[key];
}
}
database/database.module.ts
import { Module } from '#nestjs/common';
import { databaseProviders } from './database.providers';
#Module({
imports: [...databaseProviders],
exports: [...databaseProviders],
})
export class DatabaseModule {
}
database/database.providers.ts
import { ConfigModule } from '../config/config.module';
import { ConfigService } from '../config/config.service';
import { MongooseModule } from '#nestjs/mongoose';
export const databaseProviders = [
MongooseModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (config: ConfigService) => ({
uri: config.get('MONGODB_URI'),
useNewUrlParser: true,
}),
}),
];

Related

process.env's are undefined - NestJS

I've decided to write here because I've ran out of ideas. I have a NestJS app in which I use env's - nothing unusual. But something strange happens when I want to use them. I also have my own parser of these values which returns them in a convenient object - that's the first file:
env.ts
const parseStringEnv = (name: string) => {
const value: string = process.env[name];
if (!value) {
throw new Error(`Invalid env ${name}`);
}
return value;
};
const parseIntEnv = (name: string) => {
const value: string = process.env[name];
const int: number = parseInt(value);
if (isNaN(int)) {
throw new Error(`Invalid env ${name}`);
}
return int;
};
const parseBoolEnv = (name: string) => {
const value: string = process.env[name];
if (value === "false") {
return false;
}
if (value === "true") {
return true;
}
throw new Error(`Invalid env ${name}`);
};
const parseMongoString = (): string => {
const host = parseStringEnv("DATABASE_HOST");
const port = parseStringEnv("DATABASE_PORT");
const user = parseStringEnv("DATABASE_USER");
const pwd = parseStringEnv("DATABASE_PWD");
const dbname = parseStringEnv("DATABASE_NAME");
return `mongodb://${user}:${pwd}#${host}:${port}/${dbname}?authSource=admin&ssl=false`;
};
export const env = {
JWT_SECRET: parseStringEnv("JWT_SECRET"),
PORT_BACKEND: parseIntEnv("PORT_BACKEND"),
CLIENT_HOST: parseStringEnv("CLIENT_HOST"),
ENABLE_CORS: parseBoolEnv("ENABLE_CORS"),
MONGO_URI: parseMongoString(),
};
export type Env = typeof env;
I want to use it for setting port on which the app runs on and also the connection parameters for Mongoose:
In main.ts:
<rest of the code>
await app.listen(env.PORT_BACKEND || 8080);
<rest of the code>
Now, the magic starts here - the app starts just fine when ONLY ConfigModule is being imported. It will also start without ConfigModule and with require('doting').config() added. When I add MongooseModule, the app crashes because it can't parse env - and the best thing is that exception thrown has nothing to do with env's that are used to create MONGO_URI!! I'm getting "Invalid env JWT_SECRET" from my parser.
In app.module.ts
import { Module } from "#nestjs/common";
import { ConfigModule } from "#nestjs/config";
import { MongooseModule } from "#nestjs/mongoose";
import { AppController } from "./app.controller";
import { env } from "./common/env";
#Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
MongooseModule.forRoot(env.MONGO_URI), //WTF?
],
controllers: [AppController],
})
export class AppModule {}
I've honestly just ran out of ideas what could be wrong. The parser worked just fine in my last project (but I haven't used Mongoose so maybe that's what causes issues). Below is my .env file template.
JWT_SECRET=
ENABLE_CORS=
PORT_BACKEND=
DATABASE_HOST=
DATABASE_PORT=
DATABASE_USER=
DATABASE_PWD
DATABASE_NAME=
CLIENT_HOST=
Thanks for everyone who has spent their time trying to help me ;)
What's happening is you're importing env.ts before the ConfigModule has imported and set the variables in your .env file.
This is why calling require('dotenv').config() works. Under the hood, that's what the ConfigModule is doing for you. However, your call to ConfigModule.forRoot is happening after you import env.ts, so the .env file hasn't been imported yet and those variables don't yet exist.
I would highly recommend you take a look at custom configuration files, which handles this for you the "Nest way":
From the Nest docs, but note that you could also use the env.ts file you already have:
// env.ts
export default () => ({
// Add your own properties here however you'd like
port: parseInt(process.env.PORT, 10) || 3000,
database: {
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10) || 5432
}
});
And then modify your AppModule to the following. Note that we're using the forRootAsync so that we can get a handle to the ConfigService and grab the variable from that.
// app.module.ts
import configuration from './common/env';
#Module({
imports: [
ConfigModule.forRoot({
load: [configuration],
}),
//
MongooseModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
uri: configService.get<string>('MONGO_URI'),
}),
inject: [ConfigService],
});
],
})
export class AppModule {}
As an alternative, you could also just call require('dotenv').config() inside your env.ts file at the top, but you'll miss out on all the ConfigModule helpers like dev/prod .env files.
By using registerAsync of JWT module and read process.env inside useFactory method worked for me
#Module({
imports: [
JwtModule.registerAsync({
useFactory: () => ({
secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: 3600 },
}),
})
],
controllers: [AppController],
})
In my case just need to replace the order import module.
import { Module } from "#nestjs/common";
import { ConfigModule } from "#nestjs/config";
import { MongooseModule } from "#nestjs/mongoose";
import { AppController } from "./app.controller";
import { env } from "./common/env"; // call process.env.xxx here > undefined
#Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}), // process.env.xxx must be called after this line
MongooseModule.forRoot(env.MONGO_URI),
],
controllers: [AppController],
})
export class AppModule {}
so fix
import { Module } from "#nestjs/common";
import { ConfigModule } from "#nestjs/config";
// should place this at very first line
const envModule = ConfigModule.forRoot({
isGlobal: true,
})
import { MongooseModule } from "#nestjs/mongoose";
import { AppController } from "./app.controller";
import { env } from "./common/env";
#Module({
imports: [
envModule,
MongooseModule.forRoot(env.MONGO_URI),
],
controllers: [AppController],
})
export class AppModule {}
In my case I downgraded #types/node to be the same version as my node version. Could be a hint.

NestJS - How to register dynamic module provider multiple times using different configuration?

I have a knex module which is implemented like this:
import { DynamicModule, Module } from '#nestjs/common';
import { Knex, knex } from 'knex';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
export const KNEX_MODULE = 'KNEX_MODULE';
#Module({})
export class KnexModule {
static register(options: Knex.Config): DynamicModule {
return {
module: KnexModule,
providers: [
{
inject: [WINSTON_MODULE_PROVIDER],
provide: KNEX_MODULE,
useFactory: (logger: Logger) => {
logger.info('Creating new knex instance', {
context: KnexModule.name,
tags: ['instance', 'knex', 'create'],
});
return knex(options);
},
},
],
exports: [KNEX_MODULE],
};
}
}
My application requires access to multiple databases, I know I can do that by creating multiple knex instances. So I tried to register the module twice, passing different configurations. However, the module only registered once. The second register call seems to be reusing the existing object instead of creating a new knex instance.
What is the correct way to generate multiple providers, depending on the configuration passed? The closest thing I found is the forFeature functions in typeORM and Sequelize
I just found the solution. I was thinking the wrong way. I needed to register two providers to my Module. Not create two instances of my module. I solved it by adding one more parameter to my module, which is the provider token. Now it correctly creates the two providers.
import { DynamicModule, Module } from '#nestjs/common';
import { Knex, knex } from 'knex';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
export const KNEX_MODULE = 'KNEX_MODULE';
#Module({})
export class KnexModule {
static register(token: string, options: Knex.Config): DynamicModule {
return {
module: KnexModule,
providers: [
{
inject: [WINSTON_MODULE_PROVIDER],
provide: token,
useFactory: (logger: Logger) => {
logger.info('Creating new knex instance', {
context: KnexModule.name,
tags: ['instance', 'knex', 'create'],
});
return knex(options);
},
},
],
exports: [token],
};
}
}
And whenever I want to use it I register it like this:
#Module({
imports: [KnexModule.register(CatRepository.KNEX_TOKEN, knexConfigs)],
providers: [CatRepository, CatService],
controllers: [CatController],
exports: [CatService],
})
export class CatModule {}
Then in the repository I can inject the knex instance of the cats database.
#Injectable()
export class CatRepository implements Repository<Cat> {
// eslint-disable-next-line no-useless-constructor
public static KNEX_TOKEN = 'KNEX_CATS_TOKEN';
// eslint-disable-next-line no-useless-constructor
constructor(
#Inject(CatRepository.KNEX_TOKEN)
protected knex: Knex,
) {}
...
}

Using nestjs DI with class validator

Seems like a real pain in the brain...
There is a huge thread about this on github and other sites, many of them come down to using useContainer from the 'class-validator' but it does not work for me.
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
useContainer(app, { fallback: true });
await app.listen(3000);
}
bootstrap();
Here's the injectable:
#ValidatorConstraint({ name: 'uniqueOnDatabase', async: true })
#Injectable()
export class UniqueOnDatabase implements ValidatorConstraintInterface {
constructor(
private readonly userService: UserService,
) {}
public async validate(val: any, args: ValidationArguments): Promise<boolean> {
const user = await this.userService.retrieveOneByEmail(val);
return !user;
}
public defaultMessage(args: ValidationArguments): string {
return `User with such an email address already exists in the DB`;
}
}
All I want to do is use my userService inside that UniqueOnDatabase class.
Here is the module where I am providing the UniqueOnDatabase:
import { Module, CacheModule } from '#nestjs/common';
import { ConfigModule } from '#nestjs/config';
import { CacheConfigService } from 'src/config/cache/config.service';
import { CacheService } from './services/cache.service';
import { CodeGenService } from './services/code-gen.service';
import { UserExistanceValidationPipe } from './pipes/user-existance.validation.pipe';
import { UsersModule } from 'src/users/users.module';
import { UniqueOnDatabase } from './validators/unique-on-database.validator';
#Module({
providers: [
CacheService,
CodeGenService,
UniqueOnDatabase,
],
imports: [
CacheModule.registerAsync({
imports: [ConfigModule],
useClass: CacheConfigService,
}),
UsersModule,
],
exports: [
CacheService,
CodeGenService,
UniqueOnDatabase,
],
})
export class SharedModule {}
Thanks #Albert for answering your question.
Adding #Albert's answer just in case someone misses the comments:
#JayMcDoniel Aaah, seems like I've figured out the solution. I should
have used useContainer(app.select(SharedModule), { fallbackOnErrors:
true }); instead of what I did at first...
Thanks again #Albert

How to make a dynamic module a global module?

In my application, I want to use a single instance of service class throughout my project. The service class will be initialized by a dynamic module.
Things in detail
I have module called LoggerModule which has a static function register. We use this method to initialize in the app module Eg:LoggerModule.register(options). In the register method, we will be returning a dynamic module which will set this options as a custom provider.
Eg:
return {
module: LoggerModule,
providers: [
{
provide: CONFIG_OPTIONS,
useValue: options,
},
LoggerService,
],
exports: [LoggerService],
};
Here we have a LoggerService that injects the CONFIG_OPTIONS, so that we can fetch options using the service class. Now I want to be able to access the service from anywhere in the project by injecting it in my class, but since the module is not global, currently I will have to include LoggerModule.register() in all the modules that I am using. I tried using the #Global() annotation, but it doesn't seem to work.
Can you suggest any methods on how to do this? If yes please share with me an example?
All you should need to do to make a dynamic module global is add the #Global() decorator above the #Module() decorator. Same with any other module you are working with. Here are the docs on it
Edit 11/22/19
Okay, I tried to mimic your setup as close as I could with what was given and what I still had lying around my local machine. I was able to get a global module working with the following setup:
Config Module (what will be the global module)
import { DynamicModule, Module, Provider, Global } from '#nestjs/common';
import { CONFIG_MODULE_OPTIONS } from './config.constants';
import { createConfigProvider } from './config.provider';
import { ConfigService } from './config.service';
import {
ConfigModuleAsyncOptions,
ConfigModuleOptions,
ConfigOptionsFactory,
} from './interfaces/config-options.interface';
#Global()
#Module({})
export class ConfigModule {
static forRoot(options: ConfigModuleOptions): DynamicModule {
return {
module: ConfigModule,
providers: [ConfigService, ...createConfigProvider(options)],
exports: [ConfigService],
};
}
static forRootAsync(options: ConfigModuleAsyncOptions): DynamicModule {
return {
module: ConfigModule,
imports: options.imports || [],
providers: [ConfigService, ...this.createAsyncProviders(options)],
exports: [ConfigService],
};
}
private static createAsyncProviders(
options: ConfigModuleAsyncOptions,
): Provider[] {
if (options.useExisting || options.useFactory) {
return [this.createAsyncOptionsProviders(options)];
}
if (options.useClass) {
return [
this.createAsyncOptionsProviders(options),
{
provide: options.useClass,
useClass: options.useClass,
},
];
}
throw new Error('Invalid ConfigModule configuration.');
}
private static createAsyncOptionsProviders(
options: ConfigModuleAsyncOptions,
): Provider {
if (options.useFactory) {
return {
provide: CONFIG_MODULE_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
};
}
return {
provide: CONFIG_MODULE_OPTIONS,
useFactory: async (optionsFactory: ConfigOptionsFactory) =>
await optionsFactory.createConfigOptions(),
inject: [options.useExisting || options.useClass || ''],
};
}
}
I had this set up for a completely reusable Nest Module but scrapped the idea as there are already a few config modules out there, hence all the boilerplate.
Dyanmic Module (yes I know it's spelled wrong)
import { Module } from '#nestjs/common';
import { DyanmicTestService } from './dyanmic-test.service';
import { DyanmicTestController } from './dyanmic-test.controller';
#Module({
providers: [DyanmicTestService],
controllers: [DyanmicTestController],
})
export class DyanmicTestModule {}
The Dyanmic Service injects the Config Service, but notice we don't import the Config Module here. That's because it is global, and once registered in the App Module, it doesn't need to be imported anywhere else.
App Module
import { Module } from '#nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { DyanmicTestModule } from './dyanmic-test/dyanmic-test.module';
import { ConfigModule } from './config/config.module';
#Module({
imports: [
ConfigModule.forRootAsync({
useFactory: () => ({
fileName: '.env',
useProcess: false,
}),
}),
DyanmicTestModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
All of this code can also be found on my GitHub.

Dependency injection - app.get(UploadService) on INestApplication doesn't resolve its dependencies

I am trying to create external CLI, which uses my Nest context. I've made entry-point, which creates app with NestFactory.create. After accessing service with app.get, the service exists and works itself. The problem is, that it doesn't resolve any of its dependencies. There is no example on Docs and neither have I found anything related to this issue on Internet.
I am using the newest version of Nest.js on Node 10 as on 17.07.2019.
main.ts
import { NestFactory } from '#nestjs/core';
import { AppModule } from '../app.module';
import { UploadService } from 'src/api/upload/upload.service';
import { UploadModule } from 'src/api/upload/upload.module';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
await app.init();
const service: UploadService = app.select(UploadModule).get(UploadService);
console.log(service); // OK - returns UploadService instance
console.log(service.uploadModel); // X - returns `undefined`
console.log(service.configService); // X - returns `undefined`
}
bootstrap();
app.module.ts
import { Module } from '#nestjs/common';
import { MongooseModule } from '#nestjs/mongoose';
import { ConfigModule } from './config/config.module';
import { ConfigService } from './config/config.service';
import { AwsModule } from './services/aws/aws.module';
import { UploadModule } from './api/upload/upload.module';
#Module({
imports: [
ConfigModule,
MongooseModule.forRootAsync({
useFactory: async (configService: ConfigService): Promise<object> => ({
uri: configService.data.database.mongo.uri,
useCreateIndex: true,
useNewUrlParser: true,
}),
inject: [ConfigService],
}),
AwsModule,
UploadModule,
],
})
export class AppModule {}
upload.module.ts
import { Module } from '#nestjs/common';
import { MongooseModule } from '#nestjs/mongoose';
import { AwsModule } from '../../services/aws/aws.module';
import { UploadService } from './upload.service';
import { UploadSchema } from './upload.schema';
#Module({
imports: [MongooseModule.forFeature([{ name: 'Upload', schema: UploadSchema }]), AwsModule],
providers: [UploadService],
exports: [UploadService],
})
export class UploadModule {}
upload.service.ts
import { Injectable } from '#nestjs/common';
import { AwsService } from '../../services/aws/aws.service';
import { Upload } from './upload.schema';
import { InjectModel } from '#nestjs/mongoose';
import { Model } from 'mongoose';
#Injectable()
export class UploadService {
constructor(
#InjectModel('Upload')
readonly uploadModel: Model<Upload>,
readonly awsService: AwsService,
) {}
}
Expected outputs of
console.log(service.uploadModel); // X - returns `undefined`
console.log(service.configService); // X - returns `undefined`
are model/service instances. Unfortunately they return both undefined, as the dependency-injection doesn't take a place.
I tried what You did, and in nestjs version: 7.6 it works as expected.
So my assumption is they fixed it in meantime.
I had the same issue. The docs are actually pretty clear about this: https://docs.nestjs.com/standalone-applications
But I actually run into the same problem and I dont think this an intended behaviour. I tried removing a dependency after another to see if something changes.
I had request scoped services injected. As long there was an "unresolvable dependency" (request-scoped ones) all injected dependencies were undefined. I'd expect a "dependency cannot be resolved" error but it just silently failed.
See also https://github.com/nestjs/nest/issues/4630
Make sure all your dependencies can be created successfully within the correct scope for standalone apps.
To get a service with for example request-scoped dependencies use module.resolve(Service) instead of get.

Resources