DocumentationReference

HazelJS TypeORM Package

npm downloads

@hazeljs/typeorm provides TypeORM integration for HazelJS with an injectable DataSource, base repository pattern, and automatic connection lifecycle.

Quick Reference

  • Purpose: @hazeljs/typeorm integrates TypeORM into HazelJS with injectable DataSource, repository pattern, and automatic connection lifecycle management.
  • When to use: Use @hazeljs/typeorm when a HazelJS application needs database access via TypeORM. Use @hazeljs/prisma for 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., pg for 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: true in 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 TypeOrmService with lifecycle hooks
  • Repository patternBaseRepository<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

  1. TypeOrmModule — Registers TypeOrmService; optional forRoot(options) for custom DataSource config
  2. TypeOrmService — Injectable wrapper around TypeORM DataSource; onModuleInit / onModuleDestroy, getRepository(entity)
  3. BaseRepository — Abstract base class for entity repositories with standard CRUD
  4. Repository decorator@Repository({ model: 'User' }) for metadata and DI
  5. 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 LevelPrevents
READ COMMITTED (default)Dirty reads
REPEATABLE READDirty reads, non-repeatable reads
SERIALIZABLEDirty 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

  1. Use entities — Define TypeORM entities with decorators; register them in DataSource options (or use globs in forRoot).
  2. One repository per entity — Extend BaseRepository<YourEntity> and add custom methods.
  3. Use @InjectRepository — Let the container inject repositories so they stay testable.
  4. Transactions — Use dataSource.transaction() for multi-step operations that must be atomic.
  5. Avoid synchronize in production — Use migrations; set synchronize: false in 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 {}