Defining services
The empty-subclass pattern, generics, and how to add custom methods without breaking the framework wiring.
The SqlBaseCrudService base class is designed so the vast majority of services are empty subclasses. The connection, dialect, table, and per-entity configuration are all injected by forFeature — your service file usually contains no logic.
The minimum
import { SqlBaseCrudService } from 'nestjs-drizzle-crud';
import type { User } from '../db/schema';
export class UsersService extends SqlBaseCrudService<User> {}The three remaining generics default to Partial<Entity>:
// Equivalent to:
class extends <, <>, <>, <>> {}
// Supply all four generics, or rely on the inferred defaults.With explicit DTOs
When you want to decouple your input shape from your entity (recommended for public APIs):
import { SqlBaseCrudService } from 'nestjs-drizzle-crud';
import type { User } from '../db/schema';
export interface CreateUserDto { name: string; email: string }
export interface UpdateUserDto { name?: string; email?: string }
export interface UserFilterDto { name?: string; email?: string }
export class UsersService extends SqlBaseCrudService<
User,
CreateUserDto,
UpdateUserDto,
UserFilterDto
> {}The FilterDto is not used to auto-build a query — it's the type
signature of the findAll filter argument. Use it to constrain which
columns can be filtered from controllers. See
Filtering for the actual filter syntax.
Adding custom methods
Override nothing — just add your own methods. They have full access to this.config (the resolved SqlCrudConfig) and this.config.db (the Drizzle instance), so anything you can do in raw Drizzle is available inside the service.
import { eq } from 'drizzle-orm';
import { SqlBaseCrudService } from 'nestjs-drizzle-crud';
import { users, type User } from '../db/schema';
export class UsersService extends SqlBaseCrudService<User> {
findByEmail(email: string) {
return this.findOne({ email } as Partial<User>);
}
async findActive() {
return this.findAll(
{ deletedAt: { isNull: true } } as Partial<User>,
{ sortBy: 'createdAt', sortOrder: 'desc' },
);
}
// Drop down to raw Drizzle for complex queries.
async findWithPostCount() {
return this.config.db
.select({
id: users.id,
name: users.name,
postCount: sql<number>`count(${posts.id})`.as('post_count'),
})
.from(users)
.leftJoin(posts, eq(posts.authorId, users.id))
.groupBy(users.id);
}
}Don't inject the database yourself
A common mistake is to add a constructor to inject the Drizzle instance:
export class UsersService extends SqlBaseCrudService<User> {
constructor(
@Inject(DRIZZLE_DB) private readonly db: any, // ❌ not needed
) {
super({ /* ❌ the table doesn't go here */ });
}
}The module constructs the service for you with the correct config. If you need the Drizzle instance inside a custom method, use this.config.db — it points to the same instance.
Adding a constructor that calls super({ ... }) is a legacy pattern. The
modern pattern is an empty subclass plus forFeature({ service, table }).
The service class itself doesn't need to know about the database or the
table.
Don't add @Injectable() either
forFeature registers the service as a Nest provider automatically. Adding @Injectable() is harmless but redundant.
Don't add OnModuleInit to "warm up" the connection
DrizzleCrudModule handles connection lifecycle. The connection is established lazily on first query.
When the empty-subclass pattern isn't enough
Reach for these when the pattern breaks down:
- You need a service that queries across multiple tables. Don't try to glue this onto
SqlBaseCrudService— write a normal Nest service that injectsDRIZZLE_DBand uses raw Drizzle. - You need streaming rows.
findAllmaterializes the result. For large result sets, injectDRIZZLE_DBand use Drizzle's streaming APIs. - You need GraphQL resolvers. The service methods are plain async functions and can be called from any resolver layer.