TypeORM with NestJS
December 1, 2022This 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
andpg
packages installed
Database connection
It requires the initialization of the DataSource configuration.
// app.module.tsconst 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 {}
DataSource configuration contains elements for the connection string, migration details, etc.
// config/database.tsimport 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));
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"}}
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>
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.tsimport { 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');}}
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.tsimport '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;
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"}}
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>
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.tsimport { 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);}}
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.tsimport '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;
Entities
Entities are specified with their columns and Entity decorator.
// users.entity.tsimport { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';@Entity({ name: 'users' })export class UsersEntity {@PrimaryGeneratedColumn('uuid')public id: string;@Column({ name: 'first_name' })public firstName: string;}
Entities should be registered with forFeature
method.
// users.module@Module({imports: [TypeOrmModule.forFeature([UsersEntity])],// ...})export class UsersModule {}
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 } });}// ...}
Custom repositories should be registered as a provider.
// users.module@Module({// ...providers: [UsersService, UsersRepository],// ...})export class UsersModule {}
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 existasync createPost(authorUsername: string, message: string): Promise<Post> {const author = await this.authorRepository.create({username: authorUsername});return this.postRepository.save({ message, author_id: author.id });}}
Initialization of transactional context should happen before starting the app.
// main.tsasync function bootstrap(): Promise<void> {initializeTransactionalContext();// ...}
DataSource instance should be added to the transactional context.
const typeOrmConfig = {// ...dataSourceFactory: async (options) =>addTransactionalDataSource(new DataSource(options)).initialize()};
Boilerplate
Here is the link to the boilerplate I use for the development. It contains the examples mentioned above with more details.