DEV Community

Željko Šević
Željko Šević

Posted on • Edited on • Originally published at sevic.dev

TypeORM with NestJS

This post covers TypeORM examples with the NestJS framework, from setting up the connection with the Postgres database to working with transactions. The following snippets can be adjusted and reused with other frameworks like Express. The same applies to SQL databases.

Prerequisites

  • NestJS app bootstrapped
  • Postgres database running
  • @nestjs/typeorm, typeorm and pg packages installed

Database connection

It requires the initialization of the DataSource configuration.

// app.module.ts
const typeOrmConfig = {
  imports: [
    ConfigModule.forRoot({
      load: [databaseConfig]
    })
  ],
  inject: [ConfigService],
  useFactory: async (configService: ConfigService) =>
    configService.get('database'),
  dataSourceFactory: async (options) => new DataSource(options).initialize()
};

@Module({
  imports: [TypeOrmModule.forRootAsync(typeOrmConfig)]
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

DataSource configuration contains elements for the connection string, migration details, etc.

// config/database.ts
import path from 'path';
import { registerAs } from '@nestjs/config';
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';

export default registerAs(
  'database',
  (): PostgresConnectionOptions =>
    ({
      logging: false,
      entities: [path.resolve(`${__dirname}/../../**/**.entity{.ts,.js}`)],
      migrations: [
        path.resolve(`${__dirname}/../../../database/migrations/*{.ts,.js}`)
      ],
      migrationsRun: true,
      migrationsTableName: 'migrations',
      keepConnectionAlive: true,
      synchronize: false,
      type: 'postgres',
      host: process.env.DATABASE_HOSTNAME,
      port: Number(process.env.DATABASE_PORT),
      username: process.env.DATABASE_USERNAME,
      password: process.env.DATABASE_PASSWORD,
      database: process.env.DATABASE_NAME
    } as PostgresConnectionOptions)
);
Enter fullscreen mode Exit fullscreen mode

Migrations and seeders

Migrations are handled with the following scripts for generation, running, and reverting.

// package.json
{
  "scripts": {
    "migration:generate": "npm run typeorm -- migration:create",
    "migrate": "npm run typeorm -- migration:run -d src/common/config/ormconfig-migration.ts",
    "migrate:down": "npm run typeorm -- migration:revert -d src/common/config/ormconfig-migration.ts",
    "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

A new migration is generated at the provided path with the following command. The filename of it is in the format <TIMESTAMP>-<MIGRATION_NAME>.ts.

npm run migration:generate database/migrations/<MIGRATION_NAME>
Enter fullscreen mode Exit fullscreen mode

Here is the example for the migration which creates a new table. A table is dropped when the migration is reverted.

// database/migrations/1669833880587-create-users.ts
import { MigrationInterface, QueryRunner, Table } from 'typeorm';

export class CreateUsers1669833880587 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.createTable(
      new Table({
        name: 'users',
        columns: [
          {
            name: 'id',
            type: 'uuid',
            default: 'uuid_generate_v4()',
            generationStrategy: 'uuid',
            isGenerated: true,
            isPrimary: true
          },
          {
            name: 'first_name',
            type: 'varchar'
          }
        ]
      })
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.dropTable('users');
  }
}
Enter fullscreen mode Exit fullscreen mode

Scripts for running and reverting the migrations require a separate DataSource configuration, the migrations table name is migrations in this case. Running a migration adds a new row with the migration name while reverting removes it.

// config/ormconfig-migration.ts
import 'dotenv/config';
import * as path from 'path';
import { DataSource } from 'typeorm';

const config = new DataSource({
  type: 'postgres',
  host: process.env.DATABASE_HOSTNAME,
  port: Number(process.env.DATABASE_PORT),
  username: process.env.DATABASE_USERNAME,
  password: process.env.DATABASE_PASSWORD,
  database: process.env.DATABASE_NAME,
  entities: [path.resolve(`${__dirname}/../../**/**.entity{.ts,.js}`)],
  migrations: [
    path.resolve(`${__dirname}/../../../database/migrations/*{.ts,.js}`)
  ],
  migrationsTableName: 'migrations',
  logging: true,
  synchronize: false
});

export default config;
Enter fullscreen mode Exit fullscreen mode

Seeder is a type of migration, seeders are handled with the following scripts for generation, running, and reverting.

// package.json
{
  "scripts": {
    "seed:generate": "npm run typeorm -- migration:create",
    "seed": "npm run typeorm -- migration:run -d src/common/config/ormconfig-seeder.ts",
    "seed:down": "npm run typeorm -- migration:revert -d src/common/config/ormconfig-seeder.ts"
  }
}
Enter fullscreen mode Exit fullscreen mode

A new seeder is generated at the provided path with the following command. The filename of it is in the format <TIMESTAMP>-<SEEDER_NAME>.ts.

npm run seeder:generate database/seeders/<SEEDER_NAME>
Enter fullscreen mode Exit fullscreen mode

Here is the example for the seeder which inserts some data. A table data is removed when the seeder is reverted.

// database/seeders/1669834539569-add-users.ts
import { UsersEntity } from '../../src/modules/users/users.entity';
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddUsers1669834539569 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.manager.insert(UsersEntity, [
      {
        firstName: 'tester'
      }
    ]);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.manager.clear(UsersEntity);
  }
}
Enter fullscreen mode Exit fullscreen mode

Scripts for running and reverting the seeders require a separate DataSource configuration, the seeders table name is seeders in this case. Running a seeder adds a new row with the seeder name while reverting removes it.

// config/ormconfig-seeder.ts
import 'dotenv/config';
import * as path from 'path';
import { DataSource } from 'typeorm';

const config = new DataSource({
  type: 'postgres',
  host: process.env.DATABASE_HOSTNAME,
  port: Number(process.env.DATABASE_PORT),
  username: process.env.DATABASE_USERNAME,
  password: process.env.DATABASE_PASSWORD,
  database: process.env.DATABASE_NAME,
  entities: [path.resolve(`${__dirname}/../../**/**.entity{.ts,.js}`)],
  migrations: [
    path.resolve(`${__dirname}/../../../database/seeders/*{.ts,.js}`)
  ],
  migrationsTableName: 'seeders',
  logging: true,
  synchronize: false
});

export default config;
Enter fullscreen mode Exit fullscreen mode

Entities

Entities are specified with their columns and Entity decorator.

// users.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity({ name: 'users' })
export class UsersEntity {
  @PrimaryGeneratedColumn('uuid')
  public id: string;

  @Column({ name: 'first_name' })
  public firstName: string;
}
Enter fullscreen mode Exit fullscreen mode

Entities should be registered with forFeature method.

// users.module
@Module({
  imports: [TypeOrmModule.forFeature([UsersEntity])],
  // ...
})
export class UsersModule {}
Enter fullscreen mode Exit fullscreen mode

Custom repositories

Custom repositories extend the base repository class and enrich it with several additional methods.

// users.repository.ts
@Injectable()
export class UsersRepository extends Repository<UsersEntity> {
  constructor(private dataSource: DataSource) {
    super(UsersEntity, dataSource.createEntityManager());
  }

  async getById(id: string) {
    return this.findOne({ where: { id } });
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Custom repositories should be registered as a provider.

// users.module
@Module({
  // ...
  providers: [UsersService, UsersRepository],
  // ...
})
export class UsersModule {}
Enter fullscreen mode Exit fullscreen mode

Testing custom repositories

Testing custom repositories (NestJS/TypeORM) post covers more details about the unit and integration testing.

Transactions

typeorm-transactional library uses CLS (Continuation Local Storage) to handle and propagate transactions between different repositories and service methods.

@Injectable()
export class PostService {
  constructor(
    private readonly authorRepository: AuthorRepository,
    private readonly postRepository: PostRepository
  ) {}

  @Transactional() // will open a transaction if one doesn't already exist
  async createPost(authorUsername: string, message: string): Promise<Post> {
    const author = await this.authorRepository.create({
      username: authorUsername
    });
    return this.postRepository.save({ message, author_id: author.id });
  }
}
Enter fullscreen mode Exit fullscreen mode

Initialization of transactional context should happen before starting the app.

// main.ts
async function bootstrap(): Promise<void> {
  initializeTransactionalContext();
  // ...
}
Enter fullscreen mode Exit fullscreen mode

DataSource instance should be added to the transactional context.

const typeOrmConfig = {
  // ...
  dataSourceFactory: async (options) =>
    addTransactionalDataSource(new DataSource(options)).initialize()
};
Enter fullscreen mode Exit fullscreen mode

Boilerplate

Here is the link to the boilerplate I use for the development. It contains the examples mentioned above with more details.

Top comments (0)