Skip to content

NestJS Integration

Why NestJS Integration Exists

NestJS uses dependency injection (DI) to wire services together. When you write constructor(private readonly userService: UserService), NestJS looks up UserService in its DI container and provides an instance. This works beautifully for your own classes, but an ORM's EntityManager and repositories are not NestJS services by default. The framework does not know how to create them, when to initialize the database connection, or when to shut it down.

The Stingerloom NestJS integration module solves three problems:

  1. Lifecycle management -- It creates the EntityManager, establishes the database connection when NestJS starts, and cleanly closes it when NestJS shuts down.
  2. Repository injection -- It registers each entity's repository as a NestJS provider so you can inject it with a decorator, just like any other service.
  3. Multi-database support -- When your application connects to multiple databases, it ensures each repository is wired to the correct EntityManager using scoped DI tokens.

Installation

The NestJS integration is available as a subpath export -- no extra package needed.

typescript
import { StinglerloomOrmModule } from "@stingerloom/orm/nestjs";

Quick Start

Step 1: Register the Module with forRoot()

Call forRoot() in your root AppModule. This is where the database connection is established.

typescript
// app.module.ts
import { Module } from "@nestjs/common";
import { StinglerloomOrmModule } from "@stingerloom/orm/nestjs";
import { CatsModule } from "./cats/cats.module";
import { Cat } from "./entities/cat.entity";

@Module({
  imports: [
    StinglerloomOrmModule.forRoot({
      type: "mysql",
      host: "localhost",
      port: 3306,
      username: "root",
      password: "password",
      database: "mydb",
      entities: [Cat],
      synchronize: true, // Development only
    }),
    CatsModule,
  ],
})
export class AppModule {}

What forRoot() does under the hood

When NestJS processes StinglerloomOrmModule.forRoot(options), four things happen:

  1. Creates an EntityManager -- A new EntityManager instance is created.
  2. Calls em.register(options) -- This connects to the database, scans entities, and optionally synchronizes the schema. This happens inside a NestJS factory provider, so it runs during module initialization.
  3. Registers the EntityManager as a global provider -- The EntityManager instance is placed into NestJS's DI container under a specific token. Because the module is marked global: true, this provider is available to every module in your application without re-importing.
  4. Creates a StinglerloomOrmService -- This service wraps the EntityManager and implements NestJS lifecycle hooks: OnModuleInit for setup and OnApplicationShutdown for calling propagateShutdown() to close connections and clean up resources.

The key insight: forRoot() should be called once per database connection, in your root module.

Step 2: Register Entities per Feature Module with forFeature()

typescript
// cats.module.ts
import { Module } from "@nestjs/common";
import { StinglerloomOrmModule } from "@stingerloom/orm/nestjs";
import { Cat } from "../entities/cat.entity";
import { CatsService } from "./cats.service";
import { CatsController } from "./cats.controller";

@Module({
  imports: [StinglerloomOrmModule.forFeature([Cat])],
  providers: [CatsService],
  controllers: [CatsController],
})
export class CatsModule {}

What forFeature() does under the hood

When NestJS processes StinglerloomOrmModule.forFeature([Cat]), it creates a provider for each entity:

  1. Generates a unique DI token for Cat -- a Symbol like Symbol("INJECT_REPOSITORIES_TOKEN_Cat"). This token is cached in a WeakMap keyed by the entity class, so calling forFeature([Cat]) in multiple modules always produces the same token.
  2. Creates a factory provider that receives the EntityManager (from forRoot()) and calls em.getRepository(Cat) to get a BaseRepository<Cat>.
  3. Exports the provider so it is available for injection in the module's services and controllers.

The result: when NestJS encounters @InjectRepository(Cat) in a constructor, it knows exactly which token to look up and which BaseRepository<Cat> instance to inject.

Step 3: Inject into Services

typescript
// cats.service.ts
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@stingerloom/orm/nestjs";
import { BaseRepository } from "@stingerloom/orm";
import { Cat } from "../entities/cat.entity";

@Injectable()
export class CatsService {
  constructor(
    @InjectRepository(Cat)
    private readonly catRepo: BaseRepository<Cat>,
  ) {}

  async findAll() {
    return this.catRepo.find();
  }

  async findOne(id: number) {
    return this.catRepo.findOne({ where: { id } as any });
  }

  async create(data: Partial<Cat>) {
    return this.catRepo.save(data);
  }

  async remove(id: number) {
    return this.catRepo.delete({ id });
  }
}

Decorators

@InjectRepository(Entity, connectionName?)

Injects a BaseRepository<T> for the specified entity.

typescript
@InjectRepository(Cat)
private readonly catRepo: BaseRepository<Cat>;

Why you need this decorator

In NestJS, constructor injection works by matching parameter types to DI tokens. But BaseRepository<Cat> and BaseRepository<Dog> have the same runtime type -- TypeScript generics are erased at runtime. NestJS has no way to distinguish them.

@InjectRepository(Cat) solves this by attaching a unique DI token (a Symbol) to the constructor parameter. The token is derived from the entity class itself, so @InjectRepository(Cat) and @InjectRepository(Dog) produce different tokens, and NestJS injects the correct repository for each.

Under the hood, @InjectRepository(Cat) is equivalent to @Inject(Symbol("INJECT_REPOSITORIES_TOKEN_Cat")). The decorator exists so you do not have to manage tokens manually.

BaseRepository provides: find, findOne, findWithCursor, findAndCount, save, delete, softDelete, restore, insertMany, deleteMany, count, sum, avg, min, max, explain, upsert, stream, createQueryBuilder, and more.

@InjectEntityManager(connectionName?)

Injects the EntityManager directly. Use this when you need operations beyond what the repository offers -- raw queries, transactions, subscribers, or buffer operations.

typescript
import { InjectEntityManager } from "@stingerloom/orm/nestjs";
import { EntityManager } from "@stingerloom/orm";

@Injectable()
export class CatsService {
  constructor(
    @InjectEntityManager()
    private readonly em: EntityManager,
  ) {}

  async complexQuery() {
    return this.em.query<{ count: number }>(
      "SELECT COUNT(*) as count FROM cat WHERE age > ?",
      [5],
    );
  }

  async transferWithTransaction(fromId: number, toId: number) {
    return this.em.transaction(async (txEm) => {
      // ... transactional operations
    });
  }
}

Lifecycle Management

The StinglerloomOrmService manages the EntityManager lifecycle automatically through NestJS hooks:

  • OnModuleInit -- Registers the EntityManager in an internal registry and logs initialization.
  • OnApplicationShutdown -- Calls em.propagateShutdown() which: closes all database connections, removes event listeners, clears entity subscribers, stops the QueryTracker, and shuts down plugins in reverse order.

To use NestJS shutdown hooks, enable them in main.ts:

typescript
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableShutdownHooks(); // Required for graceful shutdown
  await app.listen(3000);
}
bootstrap();

Without enableShutdownHooks(), NestJS does not call onApplicationShutdown(), and database connections may leak when the process exits.


Multi-Database Connections

Why multi-database?

Your application might store users and business data in MySQL, analytics events in PostgreSQL, and audit logs in a separate database for compliance. Each database needs its own EntityManager, its own connection, and its own set of repositories.

The challenge in a DI framework: when a service asks for BaseRepository<Event>, how does NestJS know which database to get it from? The answer is token scoping -- each connection gets its own set of DI tokens.

How token scoping works

When you call forRoot(options, "analytics"), Stingerloom creates the EntityManager under the token "STINGERLOOM_ENTITY_MANAGER_analytics" instead of the default EntityManager class token. When you call forFeature([Event], "analytics"), the repository for Event is created under Symbol("INJECT_REPOSITORIES_TOKEN_Event_analytics") instead of Symbol("INJECT_REPOSITORIES_TOKEN_Event").

This means @InjectRepository(Event) (no connection name) and @InjectRepository(Event, "analytics") resolve to completely different providers, even though they are for the same entity class.

Setup

typescript
// app.module.ts
@Module({
  imports: [
    // Default connection (MySQL)
    StinglerloomOrmModule.forRoot({
      type: "mysql",
      host: "localhost",
      database: "main_db",
      entities: [User, Post],
      // ...
    }),

    // Named connection (PostgreSQL)
    StinglerloomOrmModule.forRoot(
      {
        type: "postgres",
        host: "analytics-db.example.com",
        database: "analytics",
        entities: [Event, Metric],
        // ...
      },
      "analytics",  // connection name
    ),

    UsersModule,
    AnalyticsModule,
  ],
})
export class AppModule {}

Feature Modules

typescript
// analytics.module.ts
@Module({
  imports: [
    StinglerloomOrmModule.forFeature([Event, Metric], "analytics"),
  ],
  providers: [AnalyticsService],
})
export class AnalyticsModule {}

Injecting Named Connections

Pass the connection name as the second argument to any decorator:

typescript
// analytics.service.ts
@Injectable()
export class AnalyticsService {
  constructor(
    @InjectRepository(Event, "analytics")
    private readonly eventRepo: BaseRepository<Event>,

    @InjectEntityManager("analytics")
    private readonly analyticsEm: EntityManager,
  ) {}
}

Omitting the connection name always uses the "default" connection. This means existing single-database applications continue to work without any code changes.


Registering Event Subscribers

Register EntitySubscriber instances during module initialization:

typescript
import { Injectable, OnModuleInit, Inject } from "@nestjs/common";
import { StinglerloomOrmService } from "@stingerloom/orm/nestjs";

@Injectable()
export class CatsSubscriberService implements OnModuleInit {
  constructor(
    @Inject(StinglerloomOrmService)
    private readonly ormService: StinglerloomOrmService,
  ) {}

  onModuleInit() {
    const em = this.ormService.getEntityManager();
    em.addSubscriber(new CatAuditSubscriber());
  }
}

StinglerloomOrmService

The service is available for injection and provides direct access to the EntityManager:

typescript
@Inject(StinglerloomOrmService)
private readonly ormService: StinglerloomOrmService;

// Access EntityManager
const em = ormService.getEntityManager();

// Get any repository
const repo = ormService.getRepository(User);

Exported API

Everything is imported from @stingerloom/orm/nestjs:

ExportDescription
StinglerloomOrmModuleMain module with forRoot() and forFeature()
StinglerloomOrmServiceService with EntityManager lifecycle management
InjectRepositoryDecorator for repository injection
InjectEntityManagerDecorator for EntityManager injection
getEntityManagerToken(name?)Token helper for manual DI
getOrmServiceToken(name?)Token helper for service injection
makeInjectRepositoryToken(entity, name?)Token helper for repository providers

Next Steps

Released under the MIT License.