Skip to main content

TypeORM Setup

Overview

The starter kit uses TypeORM, a powerful Object-Relational Mapping (ORM) library, to interact with various database systems. This document explains how TypeORM is configured and used in the starter kit.

Database Configuration

Environment Variables

Configure your database connection in .env:

# Database Configuration
DB_TYPE=postgres
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE=nest_starter
DB_SYNCHRONIZE=false
DB_LOGGING=true

TypeORM Module Configuration

The starter kit configures TypeORM in the database.module.ts file:

@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: configService.get('DB_TYPE', 'postgres'),
host: configService.get('DB_HOST', 'localhost'),
port: configService.get('DB_PORT', 5432),
username: configService.get('DB_USERNAME', 'postgres'),
password: configService.get('DB_PASSWORD', 'postgres'),
database: configService.get('DB_DATABASE', 'nest_starter'),
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
migrations: [__dirname + '/../migrations/**/*{.ts,.js}'],
synchronize: configService.get('DB_SYNCHRONIZE', false),
logging: configService.get('DB_LOGGING', true),
ssl: configService.get('DB_SSL', false)
? {
rejectUnauthorized: false,
}
: false,
}),
}),
],
})
export class DatabaseModule {}

Entities

Entities represent database tables and their relationships. Here's an example of a User entity:

@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ unique: true })
email: string;

@Column()
password: string;

@Column()
firstName: string;

@Column()
lastName: string;

@Column({ default: false })
isActive: boolean;

@Column({ type: 'enum', enum: UserRole, default: UserRole.USER })
role: UserRole;

@OneToMany(() => Post, post => post.author)
posts: Post[];

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}

Repository Pattern

TypeORM leverages the repository pattern to manage entities. Here's how to use it:

@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}

findAll(): Promise<User[]> {
return this.usersRepository.find();
}

findOne(id: string): Promise<User> {
return this.usersRepository.findOneBy({ id });
}

findByEmail(email: string): Promise<User> {
return this.usersRepository.findOneBy({ email });
}

async create(createUserDto: CreateUserDto): Promise<User> {
const user = this.usersRepository.create(createUserDto);
return this.usersRepository.save(user);
}

async update(id: string, updateUserDto: UpdateUserDto): Promise<User> {
await this.usersRepository.update(id, updateUserDto);
return this.findOne(id);
}

async remove(id: string): Promise<void> {
await this.usersRepository.delete(id);
}
}

TypeORM Module Registration

Register your entities in the module:

@Module({
imports: [TypeOrmModule.forFeature([User, Post, Comment])],
providers: [UsersService],
controllers: [UsersController],
exports: [UsersService],
})
export class UsersModule {}

Query Builder

For complex queries, use TypeORM's QueryBuilder:

async findUserPostsWithComments(userId: string): Promise<Post[]> {
return this.postsRepository
.createQueryBuilder('post')
.leftJoinAndSelect('post.author', 'author')
.leftJoinAndSelect('post.comments', 'comment')
.leftJoinAndSelect('comment.user', 'commentUser')
.where('author.id = :userId', { userId })
.orderBy('post.createdAt', 'DESC')
.getMany();
}

Transactions

Handle database transactions with TypeORM:

@Injectable()
export class TransferService {
constructor(
private connection: Connection,
private accountsService: AccountsService,
) {}

async transferFunds(
fromAccountId: string,
toAccountId: string,
amount: number,
): Promise<void> {
return this.connection.transaction(async (manager) => {
// Get repositories for this transaction
const accountRepo = manager.getRepository(Account);

// Deduct from source account
await accountRepo.decrement(
{ id: fromAccountId },
'balance',
amount
);

// Add to destination account
await accountRepo.increment(
{ id: toAccountId },
'balance',
amount
);

// Create transaction records
const transactionRepo = manager.getRepository(Transaction);

await transactionRepo.save([
{
accountId: fromAccountId,
type: 'DEBIT',
amount,
description: `Transfer to account ${toAccountId}`,
},
{
accountId: toAccountId,
type: 'CREDIT',
amount,
description: `Transfer from account ${fromAccountId}`,
},
]);
});
}
}

Subscribers

TypeORM subscribers allow you to listen to entity events:

@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
constructor(
private dataSource: DataSource,
private logger: Logger,
) {
dataSource.subscribers.push(this);
}

listenTo() {
return User;
}

beforeInsert(event: InsertEvent<User>) {
this.logger.log(`User is about to be created: ${event.entity.email}`);
}

afterInsert(event: InsertEvent<User>) {
this.logger.log(`User has been created: ${event.entity.email}`);
}
}

Multi-Database Support

The starter kit supports multiple database connections:

@Module({
imports: [
TypeOrmModule.forRootAsync({
name: 'default',
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
// Primary database config
}),
}),
TypeOrmModule.forRootAsync({
name: 'reporting',
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
// Secondary database config
}),
}),
],
})
export class DatabaseModule {}

To use a specific connection:

@Injectable()
export class ReportService {
constructor(
@InjectRepository(Report, 'reporting')
private reportRepository: Repository<Report>,
) {}

// Methods using reportRepository...
}

Testing with TypeORM

The starter kit includes utilities for testing with TypeORM:

describe('UsersService', () => {
let service: UsersService;
let repository: Repository<User>;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User],
synchronize: true,
}),
TypeOrmModule.forFeature([User]),
],
providers: [UsersService],
}).compile();

service = module.get<UsersService>(UsersService);
repository = module.get<Repository<User>>(getRepositoryToken(User));
});

it('should find a user by email', async () => {
const user = repository.create({
email: 'test@example.com',
password: 'password',
firstName: 'Test',
lastName: 'User',
});

await repository.save(user);

const found = await service.findByEmail('test@example.com');
expect(found).toBeDefined();
expect(found.email).toEqual('test@example.com');
});
});

Best Practices

  1. Avoid synchronize: true in Production: This option can lead to data loss in production environments
  2. Use Migrations for Schema Changes: Always use migrations to modify your database schema in production
  3. Implement Entity Validation: Use class-validator and class-transformer for entity validation
  4. Use Indexes for Performance: Add appropriate indexes to your entities for better query performance
  5. Handle Relations Carefully: Be aware of eager and lazy loading behavior with relations
  6. Use Query Builder for Complex Queries: Leverage QueryBuilder for complex queries instead of raw SQL
  7. Implement Soft Deletes: Consider using soft deletes for entities that shouldn't be permanently removed