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:
- Lifecycle management -- It creates the EntityManager, establishes the database connection when NestJS starts, and cleanly closes it when NestJS shuts down.
- 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.
- 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.
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.
// 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:
- Creates an EntityManager -- A new
EntityManagerinstance is created. - 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. - Registers the EntityManager as a global provider -- The
EntityManagerinstance is placed into NestJS's DI container under a specific token. Because the module is markedglobal: true, this provider is available to every module in your application without re-importing. - Creates a StinglerloomOrmService -- This service wraps the EntityManager and implements NestJS lifecycle hooks:
OnModuleInitfor setup andOnApplicationShutdownfor callingpropagateShutdown()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()
// 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:
- Generates a unique DI token for
Cat-- a Symbol likeSymbol("INJECT_REPOSITORIES_TOKEN_Cat"). This token is cached in a WeakMap keyed by the entity class, so callingforFeature([Cat])in multiple modules always produces the same token. - Creates a factory provider that receives the EntityManager (from
forRoot()) and callsem.getRepository(Cat)to get aBaseRepository<Cat>. - 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
// 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.
@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.
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:
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
// 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
// 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:
// 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:
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:
@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:
| Export | Description |
|---|---|
StinglerloomOrmModule | Main module with forRoot() and forFeature() |
StinglerloomOrmService | Service with EntityManager lifecycle management |
InjectRepository | Decorator for repository injection |
InjectEntityManager | Decorator 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
- EntityManager -- Full CRUD API reference
- Events & Subscribers -- EntitySubscriber pattern in detail
- Configuration -- Connection pooling, read replicas, and more
- Multi-Tenancy -- Per-tenant schema isolation with NestJS