TypeORM Package

npm downloads

The @hazeljs/typeorm package provides TypeORM integration for HazelJS applications. It includes an injectable DataSource service, base repository pattern, and automatic connection lifecycle—connect on module init, disconnect on destroy.

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