DEV Community

Cover image for TypeORM with NestJS
Ž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
  • Node.js version 26
  • NestJS 11, TypeORM 1.0, @nestjs/typeorm 11, and pg packages installed

Database connection

It requires the initialization of the DataSource configuration.

// app.module.ts
TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (configService: ConfigService) =>
    configService.get('database'),
  dataSourceFactory: async (options) => new DataSource(options).initialize()
});
Enter fullscreen mode Exit fullscreen mode

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

// config/database.ts
import { registerAs } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';

export default registerAs(
  'database',
  (): TypeOrmModuleOptions => ({
    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,
    logging: false,
    synchronize: false,
    migrationsRun: false,
    autoLoadEntities: true
  })
);
Enter fullscreen mode Exit fullscreen mode

autoLoadEntities registers entities from TypeOrmModule.forFeature() calls. Run migrations with the CLI before starting the app, or set migrationsRun: true and list migration classes in the DataSource options.

Migrations and seeders

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

// package.json
{
  "scripts": {
    "migration:create": "npm run typeorm -- migration:create",
    "migrate": "npm run typeorm -- migration:run -d src/config/ormconfig-migration.ts",
    "migrate:down": "npm run typeorm -- migration:revert -d src/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 template is created at the provided path with the following command. The filename is in the format <TIMESTAMP>-<MIGRATION_NAME>.ts.

npm run migration:create 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.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);

    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. Import migration classes explicitly - glob paths are unreliable with the TypeORM CLI.

// config/ormconfig-migration.ts
import 'dotenv/config';
import { DataSource } from 'typeorm';
import { CreateUsers1669833880587 } from '../../database/migrations/1669833880587-create-users';
import { UsersEntity } from '../users/users.entity';

export default 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: [UsersEntity],
  migrations: [CreateUsers1669833880587],
  migrationsTableName: 'migrations',
  logging: true,
  synchronize: false
});
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

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.

// config/ormconfig-seeder.ts
import 'dotenv/config';
import { DataSource } from 'typeorm';
import { AddUsers1669834539569 } from '../../database/seeders/1669834539569-add-users';
import { UsersEntity } from '../users/users.entity';

export default 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: [UsersEntity],
  migrations: [AddUsers1669834539569],
  migrationsTableName: 'seeders',
  logging: true,
  synchronize: false
});
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')
  id: string;

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

Entities should be registered with the forFeature method.

// users.module.ts
@Module({
  imports: [TypeOrmModule.forFeature([UsersEntity])],
  providers: [UsersService, UsersRepository]
})
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.ts
@Module({
  imports: [TypeOrmModule.forFeature([UsersEntity])],
  providers: [UsersService, UsersRepository]
})
export class UsersModule {}
Enter fullscreen mode Exit fullscreen mode

Testing custom repositories

Testing custom repositories (NestJS/TypeORM) post covers unit and integration testing. Runnable unit test lives in the testing-custom-repositories-demo folder.

Transactions

typeorm-transactional uses CLS or AsyncLocalStorage to propagate transactions across repositories and service methods. It supports the TypeORM DataSource API and works with custom repositories.

npm install typeorm-transactional
Enter fullscreen mode Exit fullscreen mode
@Injectable()
export class PostService {
  constructor(
    private readonly authorRepository: AuthorRepository,
    private readonly postRepository: PostRepository
  ) {}

  @Transactional()
  async createPost(authorUsername: string, message: string): Promise<PostEntity> {
    const author = this.authorRepository.create({ username: authorUsername });
    await this.authorRepository.save(author);

    return this.postRepository.save({
      message,
      authorId: author.id
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Initialize the transactional context before the NestJS app starts.

// main.ts
import {
  initializeTransactionalContext,
  StorageDriver
} from 'typeorm-transactional';

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

Register the DataSource with the transactional context.

import { addTransactionalDataSource } from 'typeorm-transactional';

dataSourceFactory: async (options) => {
  if (!options) {
    throw new Error('Invalid TypeORM options');
  }

  return addTransactionalDataSource(new DataSource(options)).initialize();
}
Enter fullscreen mode Exit fullscreen mode

TypeORM also exposes dataSource.transaction() if you prefer explicit transaction blocks without a decorator.

Demo

Sample NestJS app for this post lives in the typeorm-nestjs-demo folder. Get access via code demos.

Top comments (0)