Skip to content
NewFrom Mongoose to SQL6 min read

NestJS

uql-orm/nestjs registers your querier pool with Nest’s dependency injection and ends it when the app shuts down. It supports Nest 10 to 12 and is tested against Nest 12.

app.module.ts
import { Module } from '@nestjs/common';
import { UqlModule } from 'uql-orm/nestjs';
import { pool } from './uql.config.js';
@Module({
imports: [UqlModule.forRoot({ pool })], // global unless you pass global: false
})
export class AppModule {}

To build the pool from other providers, use forRootAsync:

import { ConfigModule, ConfigService } from '@nestjs/config';
UqlModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) =>
new PgQuerierPool({ connectionString: config.get('DATABASE_URL') }),
});

Call app.enableShutdownHooks() in main.ts so the pool ends on SIGTERM.

Declare your entities with defineEntity rather than decorators. Nest’s constructor injection needs experimentalDecorators: true, and one tsconfig.json cannot compile both decorator specs. defineEntity takes the same options, produces the same metadata and runs the same checks, except one foreign-key case.

CommonJS and ESM apps both work, since Node loads UQL’s ESM through require(esm). Keep module at commonjs or nodenext, as Nest scaffolds it; node16 and node18 refuse the import.

Inject the pool with the UQL_QUERIER_POOL token. Never make a Querier a provider: it holds a connection, which a singleton would pin.

import { Inject, Injectable } from '@nestjs/common';
import type { QuerierPool, Query, UniversalQuerier } from 'uql-orm';
import { UQL_QUERIER_POOL } from 'uql-orm/nestjs';
import { User } from './shared/models/index.js';
@Injectable()
export class UsersService {
constructor(@Inject(UQL_QUERIER_POOL) private readonly pool: QuerierPool) {}
findMany(q: Query<User>) {
return this.pool.findMany(User, q);
}
create(user: User, db: UniversalQuerier = this.pool) {
return db.insertOne(User, user);
}
}

create accepts a UniversalQuerier that defaults to the pool. That lets a caller run several services in one transaction by passing its querier down:

signUp(user: User) {
return this.pool.transaction(async (querier) => {
await this.users.create(user, querier);
await this.audit.record({ type: 'user.created' }, querier);
});
}

In controllers, validate the few arguments an endpoint accepts and build the query on the server, rather than taking a client’s query. In Nest 12, @Body({ schema }), @Query and @Param take a Standard Schema such as zod, which StandardSchemaValidationPipe checks.

Pass getContext and the module adds a global interceptor that runs each request inside withContext. A security filter then scopes every query in the request, relations, cascades and transactions included:

type AuthedRequest = { user: { id: string; tenantId: number } };
UqlModule.forRoot<AuthedRequest>({
pool,
getContext: (req) => ({ tenantId: req.user.tenantId, userId: req.user.id }),
});

Interceptors run after guards, so req.user is already set. The context reaches controllers and services but not guards or exception filters. If you need it earlier, say from a header, use your own middleware: withContext(context, () => next()).

Nest’s default platform is Express 5, so the Express middleware mounts in main.ts:

main.ts
import { NestFactory } from '@nestjs/core';
import { querierMiddleware } from 'uql-orm/express';
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks();
app.use('/api', querierMiddleware({ pool, include: [User, Post] }));
await app.listen(3000);

Unknown routes fall through to your controllers. On the Fastify platform, use the createRequestHandler bridge.