homecourse
 
   
🔍

TypeORM with NestJS

December 1, 2022

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.

2021

Testing custom repositories (NestJS/TypeORM)

September 5, 2021

Custom repositories extend the base repository class and enrich it with several additional methods. This post covers the unit and integration testing.

// user.repository.ts
@Injectable()
export class UserRepository extends Repository<UserEntity> {
constructor(private dataSource: DataSource) {
super(UserEntity, dataSource.createEntityManager());
}
async getById(id: string) {
return this.findOne({ where: { id } });
}
// ...
}

Setup

Inject a custom repository into the service.

// user.service.ts
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
async getById(id: string): Promise<User> {
return this.userRepository.getById(id);
}
// ...
}

Pass entity class to the forFeature method.

// user.module.ts
@Module({
imports: [
TypeOrmModule.forFeature([UserEntity])],
// ...
],
providers: [UserService, UserRepository],
// ...
})
export class UserModule {}

Unit testing

To properly unit-test the custom repository, mock some methods.

// user.repository.spec.ts
describe('UserRepository', () => {
let userRepository: UserRepository;
const dataSource = {
createEntityManager: jest.fn()
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserRepository,
{
provide: DataSource,
useValue: dataSource
}
]
}).compile();
userRepository = module.get<UserRepository>(UserRepository);
});
describe('getById', () => {
it('should return found user', async () => {
const id = 'id';
const user = {
id
};
const findOneSpy = jest
.spyOn(userRepository, 'findOne')
.mockResolvedValue(user as UserEntity);
const foundUser = await userRepository.getById(id);
expect(foundUser).toEqual(user);
expect(findOneSpy).toHaveBeenCalledWith({ where: user });
});
});
});

Integration testing

Integration testing is more suitable when working with databases. Read more about it on Integration testing Node.js apps post

Boilerplate

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