HazelJS TypeORM Package
@hazeljs/typeorm provides TypeORM integration for HazelJS with an injectable DataSource, base repository pattern, and automatic connection lifecycle.
Quick Reference
- Purpose:
@hazeljs/typeormintegrates TypeORM into HazelJS with injectable DataSource, repository pattern, and automatic connection lifecycle management. - When to use: Use
@hazeljs/typeormwhen a HazelJS application needs database access via TypeORM. Use@hazeljs/prismafor Prisma ORM instead. - Key concepts:
TypeOrmModule.forRoot(),TypeOrmService(injectable DataSource), base repository, entity decorators, automatic connect/disconnect lifecycle. - Dependencies:
@hazeljs/core,typeorm, a database driver (e.g.,pgfor PostgreSQL). - Common patterns: Register
TypeOrmModule.forRoot({ type, host, entities })→ inject repositories → use repository methods for CRUD. - Common mistakes: Not synchronizing entities with the database schema; using
synchronize: truein production (can cause data loss); not closing connections on shutdown.
Purpose
When using TypeORM you need a single DataSource, repository-style data access, and clean integration with dependency injection. The @hazeljs/typeorm package provides:
- DataSource integration — TypeORM DataSource as injectable
TypeOrmServicewith lifecycle hooks - Repository pattern —
BaseRepository<T>with find, findOne, create, save, update, delete, and count - Decorators —
@Repository({ model: 'User' })and@InjectRepository()for DI - forRoot — Optional
TypeOrmModule.forRoot(options)for custom DataSource options - Error handling — Built-in mapping of TypeORM errors (e.g. unique constraint, foreign key) to readable errors
Architecture
The package wraps TypeORM's DataSource and integrates with HazelJS dependency injection:
graph TD A["Your Services & Controllers"] --> B["BaseRepository<br/>(CRUD & Custom Methods)"] B --> C["TypeOrmService<br/>(DataSource Wrapper)"] C --> D["TypeORM DataSource<br/>(Repository, Manager)"] D --> E["Database<br/>(PostgreSQL, MySQL, etc)"] style A fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff style B fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff style C fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff style D fill:#8b5cf6,stroke:#a78bfa,stroke-width:2px,color:#fff style E fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
Key Components
- TypeOrmModule — Registers
TypeOrmService; optionalforRoot(options)for custom DataSource config - TypeOrmService — Injectable wrapper around TypeORM DataSource;
onModuleInit/onModuleDestroy,getRepository(entity) - BaseRepository — Abstract base class for entity repositories with standard CRUD
- Repository decorator —
@Repository({ model: 'User' })for metadata and DI - InjectRepository — Parameter decorator to inject repository instances
Advantages
1. Entity-based
Use TypeORM entities and decorators; no separate schema file—define models in TypeScript.
2. Repository pattern
Consistent data access with BaseRepository<T>; add custom methods per entity.
3. Lifecycle management
Connection is initialized on module init and destroyed on module destroy—no manual connect/disconnect.
4. Error handling
Base repository maps TypeORM errors (unique constraint, foreign key, not found) to clear messages.
5. DI integration
@Repository implies @Injectable() — one decorator does the job for repositories. Services use @Service(). Inject repositories via @InjectRepository(); everything works with the HazelJS container.
6. Transactions
Use TypeOrmService.dataSource.transaction() for atomic operations.
Installation
npm install @hazeljs/typeorm typeorm
Quick Start
1. Set DATABASE_URL
# .env
DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"
2. Register the module
import { HazelModule } from '@hazeljs/core';
import { TypeOrmModule } from '@hazeljs/typeorm';
@HazelModule({
imports: [TypeOrmModule],
})
export class AppModule {}
With custom options (e.g. entities, logging):
import { TypeOrmModule } from '@hazeljs/typeorm';
@HazelModule({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'user',
password: 'pass',
database: 'mydb',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: false,
}),
],
})
export class AppModule {}
3. Define an entity
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('users')
export class UserEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ unique: true })
email!: string;
@Column()
name!: string;
}
4. Create a repository
@Repository implies @Injectable() — you do not need to add both decorators.
import { BaseRepository, Repository, TypeOrmService } from '@hazeljs/typeorm';
import { UserEntity } from './user.entity';
@Repository({ model: 'User' })
export class UserRepository extends BaseRepository<UserEntity> {
constructor(typeOrm: TypeOrmService) {
super(typeOrm, UserEntity);
}
async findByEmail(email: string): Promise<UserEntity | null> {
return this.findOne({ where: { email } });
}
}
5. Use in a service
import { Service } from '@hazeljs/core';
import { InjectRepository } from '@hazeljs/typeorm';
import { UserRepository } from './user.repository';
@Service()
export class UserService {
constructor(
@InjectRepository()
private readonly userRepository: UserRepository
) {}
async findAll() {
return this.userRepository.find();
}
async create(data: { email: string; name: string }) {
return this.userRepository.create(data);
}
}
TypeOrm Service
Inject TypeOrmService when you need the DataSource or a repository directly:
import { Service } from '@hazeljs/core';
import { TypeOrmService } from '@hazeljs/typeorm';
import { UserEntity } from './user.entity';
@Service()
export class UserService {
constructor(private readonly typeOrm: TypeOrmService) {}
async findAll() {
const repo = this.typeOrm.getRepository(UserEntity);
return repo.find();
}
}
Base Repository
BaseRepository<T> exposes these methods (all delegate to TypeORM's Repository<T>):
- find(options?) — Find many entities
- findOne(options) — Find one or null
- create(data) — Create entity and save
- save(entity) — Insert or update
- update(criteria, partial) — Update by criteria
- delete(criteria) — Delete by criteria
- count(options?) — Count entities
Subclass and pass the entity class to the constructor:
@Repository({ model: 'User' })
export class UserRepository extends BaseRepository<UserEntity> {
constructor(typeOrm: TypeOrmService) {
super(typeOrm, UserEntity);
}
async findActive() {
return this.find({ where: { active: true } });
}
}
@Repository and @InjectRepository
@Repository is a class decorator that stores the entity name in metadata and implicitly registers the class as injectable. You do not need @Injectable() — @Repository does that for you.
@Repository({ model: 'User' })
export class UserRepository extends BaseRepository<UserEntity> {
constructor(typeOrm: TypeOrmService) {
super(typeOrm, UserEntity);
}
}
// ❌ Redundant — @Injectable() is already implied by @Repository
@Injectable()
@Repository({ model: 'User' })
export class UserRepository extends BaseRepository<UserEntity> { /* ... */ }
Shorthand: @Repository('User').
To use a non-singleton scope, pass scope directly to @Repository:
@Repository({ model: 'Session', scope: 'transient' })
export class SessionRepository extends BaseRepository<SessionEntity> {
constructor(typeOrm: TypeOrmService) {
super(typeOrm, SessionEntity);
}
}
@InjectRepository is a parameter decorator so the container injects the repository by type:
@Service()
export class UserService {
constructor(
@InjectRepository() private userRepository: UserRepository
) {}
}
Complete Example
Repository, service, and controller wired together:
import { Service, Controller, Get, Post, Put, Delete, Param, Body } from '@hazeljs/core';
import { BaseRepository, Repository, InjectRepository, TypeOrmService } from '@hazeljs/typeorm';
import { UserEntity } from './user.entity';
@Repository({ model: 'User' })
export class UserRepository extends BaseRepository<UserEntity> {
constructor(typeOrm: TypeOrmService) {
super(typeOrm, UserEntity);
}
async findByEmail(email: string) {
return this.findOne({ where: { email } });
}
}
@Service()
export class UserService {
constructor(
@InjectRepository() private readonly userRepository: UserRepository
) {}
async findAll() {
return this.userRepository.find();
}
async findOne(id: string) {
return this.userRepository.findOne({ where: { id } });
}
async create(data: { email: string; name: string }) {
return this.userRepository.create(data);
}
async update(id: string, data: { email?: string; name?: string }) {
await this.userRepository.update({ id }, data);
return this.userRepository.findOne({ where: { id } });
}
async delete(id: string) {
await this.userRepository.delete({ id });
}
}
@Controller('users')
export class UsersController {
constructor(private readonly userService: UserService) {}
@Get()
async findAll() {
return this.userService.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string) {
return this.userService.findOne(id);
}
@Post()
async create(@Body() body: { email: string; name: string }) {
return this.userService.create(body);
}
@Put(':id')
async update(
@Param('id') id: string,
@Body() body: { email?: string; name?: string }
) {
return this.userService.update(id, body);
}
@Delete(':id')
async delete(@Param('id') id: string) {
await this.userService.delete(id);
}
}
Transactions
TypeORM transactions guarantee that multiple database operations either all succeed or all fail together — critical for operations like transfers, order placement, or any multi-table write where partial success would leave data in an inconsistent state.
Access the transaction API through TypeOrmService.dataSource.transaction(). The callback receives an EntityManager — use it instead of repositories inside the transaction so all queries share the same database connection and transaction context.
Basic Transaction
import { Service } from '@hazeljs/core';
import { TypeOrmService } from '@hazeljs/typeorm';
import { Account } from './account.entity';
@Service()
export class TransferService {
constructor(private readonly typeOrm: TypeOrmService) {}
async transfer(fromId: string, toId: string, amount: number): Promise<void> {
await this.typeOrm.dataSource.transaction(async (manager) => {
// Both operations run in the same transaction.
// If the increment throws, the decrement is rolled back automatically.
await manager.decrement(Account, { id: fromId }, 'balance', amount);
await manager.increment(Account, { id: toId }, 'balance', amount);
});
}
}
Transaction with Multiple Entities
Use the manager to work across entity types in one atomic operation:
import { Service } from '@hazeljs/core';
import { TypeOrmService } from '@hazeljs/typeorm';
import { Order } from './order.entity';
import { OrderItem } from './order-item.entity';
import { ProductInventory } from './product-inventory.entity';
@Service()
export class OrderService {
constructor(private readonly typeOrm: TypeOrmService) {}
async placeOrder(
userId: string,
items: { productId: string; quantity: number; price: number }[],
): Promise<Order> {
return this.typeOrm.dataSource.transaction(async (manager) => {
// 1. Create the order header
const order = manager.create(Order, {
userId,
status: 'confirmed',
total: items.reduce((sum, i) => sum + i.price * i.quantity, 0),
});
await manager.save(order);
// 2. Create each line item
const lineItems = items.map((i) =>
manager.create(OrderItem, { orderId: order.id, ...i }),
);
await manager.save(lineItems);
// 3. Decrement inventory for every product
for (const item of items) {
const result = await manager.decrement(
ProductInventory,
{ productId: item.productId },
'stock',
item.quantity,
);
// Roll back the whole transaction if any product is out of stock
if (!result.affected) {
throw new Error(`Product ${item.productId} not found in inventory`);
}
}
return order;
});
}
}
Transaction with Query Builder
For more complex queries inside a transaction, use the manager's query builder:
async adjustUserBalance(userId: string, delta: number): Promise<void> {
await this.typeOrm.dataSource.transaction(async (manager) => {
// Lock the row for the duration of the transaction (SELECT ... FOR UPDATE)
const account = await manager
.createQueryBuilder(Account, 'account')
.setLock('pessimistic_write')
.where('account.userId = :userId', { userId })
.getOneOrFail();
if (delta < 0 && account.balance + delta < 0) {
throw new Error('Insufficient balance');
}
await manager.update(Account, { userId }, { balance: account.balance + delta });
});
}
Isolation Levels
Control transaction isolation when you need to prevent dirty reads, non-repeatable reads, or phantom reads:
import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel';
async criticalUpdate(id: string, data: Partial<Record>): Promise<void> {
await this.typeOrm.dataSource.transaction(
'SERIALIZABLE' as IsolationLevel,
async (manager) => {
const record = await manager.findOneByOrFail(Record, { id });
await manager.save(Record, { ...record, ...data });
},
);
}
| Isolation Level | Prevents |
|---|---|
READ COMMITTED (default) | Dirty reads |
REPEATABLE READ | Dirty reads, non-repeatable reads |
SERIALIZABLE | Dirty reads, non-repeatable reads, phantom reads |
Error Handling in Transactions
TypeORM rolls back automatically when the callback throws. Catch the error in the caller to respond appropriately:
import { Service } from '@hazeljs/core';
@Service()
export class PaymentService {
constructor(private readonly typeOrm: TypeOrmService) {}
async processPayment(orderId: string, amount: number): Promise<{ success: boolean; error?: string }> {
try {
await this.typeOrm.dataSource.transaction(async (manager) => {
const order = await manager.findOneByOrFail(Order, { id: orderId });
if (order.status !== 'pending') {
throw new Error(`Order ${orderId} is already ${order.status}`);
}
await manager.update(Order, { id: orderId }, { status: 'paid' });
await manager.save(Payment, manager.create(Payment, { orderId, amount }));
});
return { success: true };
} catch (err) {
// Transaction was automatically rolled back
return { success: false, error: (err as Error).message };
}
}
}
Error Handling
The base repository's handleError maps common TypeORM/database errors:
- 23505 — Unique constraint violation
- 23503 — Foreign key constraint violation
- EntityNotFoundError — Record not found
You can catch and handle these in services or let them bubble as readable errors.
Best Practices
- Use entities — Define TypeORM entities with decorators; register them in DataSource options (or use globs in
forRoot). - One repository per entity — Extend
BaseRepository<YourEntity>and add custom methods. - Use @InjectRepository — Let the container inject repositories so they stay testable.
- Transactions — Use
dataSource.transaction()for multi-step operations that must be atomic. - Avoid synchronize in production — Use migrations; set
synchronize: falsein production.
What's Next?
- Learn about Prisma for an alternative ORM with schema-first workflow
- Explore Cache for caching query results
- Check out Config for database configuration
Recipes
Recipe: Entity with Repository Pattern
// File: src/products/product.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
@Entity()
export class Product {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column('decimal', { precision: 10, scale: 2 })
price: number;
@Column({ default: true })
active: boolean;
@CreateDateColumn()
createdAt: Date;
}
// File: src/products/products.service.ts
import { Service } from '@hazeljs/core';
import { InjectRepository } from '@hazeljs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './product.entity';
@Service()
export class ProductsService {
constructor(
@InjectRepository(Product)
private readonly repo: Repository<Product>,
) {}
findAll() {
return this.repo.find({ where: { active: true } });
}
findOne(id: string) {
return this.repo.findOneBy({ id });
}
create(data: Partial<Product>) {
return this.repo.save(this.repo.create(data));
}
async remove(id: string) {
await this.repo.delete(id);
}
}
Recipe: Register TypeORM Module
// File: src/app.module.ts
import { HazelModule } from '@hazeljs/core';
import { TypeOrmModule } from '@hazeljs/typeorm';
import { Product } from './products/product.entity';
@HazelModule({
imports: [
TypeOrmModule.register({
type: 'postgres',
url: process.env.DATABASE_URL,
entities: [Product],
synchronize: process.env.NODE_ENV !== 'production',
}),
TypeOrmModule.forFeature([Product]),
],
})
export class AppModule {}
Related Resources
- Prisma Package – Alternative ORM
- Cache Package – Query result caching
- Config Package – Database configuration
- Auth Package – User management with TypeORM