DEV Community

Iulian Preda
Iulian Preda

Posted on

Nx NestJs - Typeorm database connection with environment variables

Continuing from the last article, we should already have a defined ConfigModule and environment variables.

To set up the data base connection in this case it's just a matter of a few lines of code.
In my example I will use a Postgresql database, but it can be any type of database.

  • Install the npm install --save @nestjs/typeorm typeorm@0.3 pg. pg is the Postgresql driver used by Typeorm to connect to the database. For each individual database it will be a different driver, e.g. sql2, mongoose and others. The typeorm we installed is v0.3, in the nestJs documentation it is recommended to use the v0.2 but it's compatible and newer, If you dislike it you can use v0.2.
  • Create in the project's folder a typeorm folder with a typeorm.service.ts file.
  • Add to the file the following:
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';

import { IEnvironment } from './../environments/env.interface';

@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  constructor(private config: ConfigService) {}

  public createTypeOrmOptions(): TypeOrmModuleOptions {
    const db = this.config.get<IEnvironment['database']>('database') as IEnvironment['database'];
    const productionMode = this.config.get<IEnvironment['production']>('production') as IEnvironment['production'];
    return {
      type: 'postgres',
      host: db.host,
      port: db.port,
      database: db.database,
      username: db.username,
      password: db.password,
      //   migrations: ['dist/migrations/*.{ts,js}'],
      logger: 'file',
      synchronize: !productionMode, // never use TRUE in production!
      autoLoadEntities: true,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Special mentions about this:

  • migrations are disabled. We will come back to them in a different article because they are managed outside of Nx and NestJs using the TypeOrm Cli.
  • the synchronize is turned on only in non-production environment

Now all that it remained is to use this class in the AppModule(or a separate DatabaseModule if you would like to have a more granular approach)

  • Go to app.module.ts and add in the imports TypeOrmModule.forRootAsync({ useClass: TypeOrmConfigService }) For example, my app.module.ts looks like this
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';

import { getEnvConfig } from './environments/env-config';
import { envValidation } from './environments/env-validator';
import { TypeOrmConfigService } from './typeorm/typeorm.service';

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [getEnvConfig],
      isGlobal: true,
      cache: true,
      validate: envValidation,
    }),
    TypeOrmModule.forRootAsync({ useClass: TypeOrmConfigService })
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

That was it, next step would be creating models and services. A good documentation about that is here

Thanks for reading!

Discussion (8)

Collapse
markasena profile image
Mark Asena

You nailed it bro, I wonder how can i run migrations on production environment with docker.

Collapse
ipreda profile image
Iulian Preda Author

Personally I wouldn't run anything from docker, it's not exacyly it's purpose.
Most probably you would do better with a custom script and config for production env.
In that way you can run local migrations without deploying the code to the docker image, and you just need the connection string and the migrations.

Collapse
markasena profile image
Mark Asena

Doing migrations has changed since type orm 0.3 old cli into typeormoptions are not there anymore. am i right?

Thread Thread
ipreda profile image
Iulian Preda Author

From what I read on their website you just need to create a class for migrations and use it in a cli command. Technically you can inject/create the options object in the same way and reuse it in that class.

Unfortunately I can't provide yet some boilerplate for that. In my current project I switched rather early on from Typeorm to MikroOrm, but in this aspect they seem rather similar so this approach should work.

The only noticeable difference is that for MikroOrm I managed to find a third party executable that is able to run for a specific project, for this you might need to either have a global config or a custom script to run from a different path.

Thread Thread
markasena profile image
Mark Asena

Looking forward into your posts about migration, i'm still trying to grasp the concepts of monorepo and all that jazz witnin this series of posts worked well for me, I also tried your approach into mirkoorm and it worked, still my concerns are on migrations, what third party tool did you use?

Thread Thread
ipreda profile image
Iulian Preda Author

I used @alexy4744/nx-mikro-orm-cli

I can actually provide some snippets for this., they might be useful for after you use the instructions from the package's page.

./mikro-orm.base.ts - at the root of the nx project I have a config that can be propagated for any number of projects

import { LoadStrategy, MikroORMOptions } from '@mikro-orm/core';
import { TSMigrationGenerator } from '@mikro-orm/migrations';
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
import { SqlHighlighter } from '@mikro-orm/sql-highlighter';

export function mikroOrmConfigBase(path = __dirname): Partial<MikroORMOptions<PostgreSqlDriver>> {
  const undocumentedConfig: any = {
    ignoreSchema: ['columnar'], // schema generated by and for citus
  };
  return {
    entities: [path + '/src/**/*.entity{.ts,.js}'],
    entitiesTs: [path + '/src/**/*.entity.ts'],
    type: 'postgresql',
    persistOnCreate: false, // we have to manually call persistAndFlush


    allowGlobalContext: true,
    forceUtcTimezone: true,
    strict: true, // disable automatic type casting
    loadStrategy: LoadStrategy.JOINED,
    highlighter: new SqlHighlighter(), // default values:
    schemaGenerator: {
      disableForeignKeys: true, // wrap statements with `set foreign_key_checks = 0` or equivalent
      createForeignKeyConstraints: true, // whether to generate FK constraints
      ...undocumentedConfig,
    },

    migrations: {
      tableName: 'mikro_orm_migrations', // name of database table with log of executed transactions
      path: path + '/src/**/data-access/migrations', // path to the folder with migrations
      pathTs: path + '/src/**/data-access/migrations', // path to the folder with TS migrations (if used, we should put path to compiled files in `path`)
      glob: '!(*.d).{js,ts}', // how to match migration files (all .js and .ts files, but not .d.ts)
      transactional: true, // wrap each migration in a transaction
      disableForeignKeys: true, // wrap statements with `set foreign_key_checks = 0` or equivalent
      allOrNothing: true, // wrap all migrations in master transaction
      dropTables: true, // allow to disable table dropping
      safe: true, // allow to disable table and column dropping
      snapshot: true, // save snapshot when creating new migrations
      emit: 'ts', // migration generation mode
      generator: TSMigrationGenerator, // migration generator, e.g. to allow custom formatting
    },

    seeder: {
      path: path + '/src/**/data-access/seeder', // path to the folder with seeders
      pathTs: path + '/src/**/data-access/seeder', // path to the folder with TS seeders (if used, we should put path to compiled files in `path`)
      defaultSeeder: 'DatabaseSeeder', // default seeder class name
      glob: '!(*.d).{js,ts}', // how to match seeder files (all .js and .ts files, but not .d.ts)
      emit: 'ts', // seeder generation mode
      fileName: (className: string) => className, // seeder file naming convention
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Please change any paths that might be different for your project.

Then in my backend project I have

/apps/backend/project.json -> under the "target" options

  "mikro-orm": {
      "executor": "@alexy4744/nx-mikro-orm-cli:run",
      "options": {
        "config": {
          "configPaths": ["./mikro-orm.config.ts"],
          "tsConfigPath": "./tsconfig.json",
          "useTsNode": true
        }
      }
    },
    "schema-create": {
      "executor": "nx:run-commands",
      "options": {
        "command": "nx mikro-orm backend --args=\"schema:create --run\""
      }
    },
    "schema-fresh": {
      "executor": "nx:run-commands",
      "options": {
        "command": "nx mikro-orm backend --args=\"schema:fresh --run --seed\""
      }
    },
    "schema-update": {
      "executor": "nx:run-commands",
      "options": {
        "command": "nx mikro-orm backend --args=\"schema:update --safe --run\""
      }
    },
    "schema-drop": {
      "executor": "nx:run-commands",
      "options": {
        "command": "nx mikro-orm backend --args=\"schema:drop --run\""
      }
    },
    "migration-initial": {
      "executor": "nx:run-commands",
      "options": {
        "command": "nx mikro-orm backend --args=\"migration:create --initial\""
      }
    },
    "migration-create": {
      "executor": "nx:run-commands",
      "options": {
        "command": "nx mikro-orm backend --args=\"migration:create\""
      }
    },
    "migration-fresh": {
      "executor": "nx:run-commands",
      "options": {
        "command": "nx mikro-orm backend --args=\"migration:fresh --seed\""
      }
    },
    "seed": {
      "executor": "nx:run-commands",
      "options": {
        "command": "nx mikro-orm backend --args=\"seeder:run\""
      }
    }
Enter fullscreen mode Exit fullscreen mode

/apps/backend/mikro-orm.ts

import { mikroOrmConfigBase } from 'mikro-orm.config.base';

import { getEnvConfig } from './src/environments/env-config';
import { env } from './src/environments/environment';

const db = getEnvConfig(env).database;

const config = {
  ...mikroOrmConfigBase(__dirname),
  host: db.host,
  port: db.port,
  dbName: db.database,
  user: db.username,
  password: db.password,
};
export default Promise.resolve(config);
Enter fullscreen mode Exit fullscreen mode

As you can see I just reused the functions from the previous articles to get the env variables from the env file or from the environment.

Also in the data acces folder which for me hosts the entities i have a DatabaseSeeder.ts file that gets the repositories and populates the data.

I cannot post that one as it is 100% custom made for my project's needs and it wouldn't be useful at all, but I can post a small snippet

export class DatabaseSeeder extends Seeder {
  async run(em: EntityManager): Promise<void> {
    const repo = em.getRepository(Entity);
    const entity = repo.create(//customEntityData);


    em.persistAndFlush(entity);
  }
}

Enter fullscreen mode Exit fullscreen mode

I hope this helps!

Thread Thread
markasena profile image
Mark Asena

There must be something wrong with webpack i tried your approach it wont detect entities on runtime thats just sad, i just made another class to do my migrations i use this approach on runtime i use your previous service inject approach, but i manually override entity locations. Thank you!

Thread Thread
ipreda profile image
Iulian Preda Author

Yes. Webpack packs everything together in a single bundle and i suppose the paths are ruined in that case.
For that Nest has the "autoLoadEntories" option vut you need to import every entity in the ORM's module for it to work.