homeresume
 
   

TypeORM with NestJS

Published December 1, 2022Last updated October 23, 20245 min read

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 {}

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)
);

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.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');
}
}

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;

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.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);
}
}

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;

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;
}

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 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 });
}
}

Initialization of transactional context should happen before starting the app.

// main.ts
async 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.