Overview
While the framework providesTypeOrmCrudService out of the box, you can create custom service implementations for other ORMs (like Mongoose, Prisma, Sequelize) or even non-database data sources (like REST APIs, GraphQL, etc.).
Creating a Custom Service
To create a custom CRUD service, extend the abstractCrudService class and implement all required methods.
Basic Structure
import { Injectable } from '@nestjs/common';
import { CrudService, CrudRequest, CreateManyDto, GetManyDefaultResponse } from '@nestjsx/crud';
@Injectable()
export class CustomCrudService<T> extends CrudService<T> {
constructor(private dataSource: any) {
super();
}
async getMany(req: CrudRequest): Promise<GetManyDefaultResponse<T> | T[]> {
// Implementation
}
async getOne(req: CrudRequest): Promise<T> {
// Implementation
}
async createOne(req: CrudRequest, dto: T | Partial<T>): Promise<T> {
// Implementation
}
async createMany(req: CrudRequest, dto: CreateManyDto<T | Partial<T>>): Promise<T[]> {
// Implementation
}
async updateOne(req: CrudRequest, dto: T | Partial<T>): Promise<T> {
// Implementation
}
async replaceOne(req: CrudRequest, dto: T | Partial<T>): Promise<T> {
// Implementation
}
async deleteOne(req: CrudRequest): Promise<void | T> {
// Implementation
}
async recoverOne(req: CrudRequest): Promise<T> {
// Implementation
}
}
Mongoose Example
Here’s a complete example of a CRUD service for Mongoose:import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, FilterQuery } from 'mongoose';
import { CrudService, CrudRequest, CreateManyDto, GetManyDefaultResponse } from '@nestjsx/crud';
import { ParsedRequestParams } from '@nestjsx/crud-request';
@Injectable()
export class MongoCrudService<T> extends CrudService<T> {
constructor(@InjectModel('ModelName') private model: Model<T>) {
super();
}
async getMany(req: CrudRequest): Promise<GetManyDefaultResponse<T> | T[]> {
const { parsed, options } = req;
// Build query
const filter = this.buildFilter(parsed);
let query = this.model.find(filter);
// Apply sorting
if (parsed.sort && parsed.sort.length) {
const sortObj = parsed.sort.reduce((acc, sort) => {
acc[sort.field] = sort.order === 'ASC' ? 1 : -1;
return acc;
}, {});
query = query.sort(sortObj);
}
// Apply pagination
if (this.decidePagination(parsed, options)) {
const limit = this.getTake(parsed, options.query);
const skip = this.getSkip(parsed, limit);
if (limit) query = query.limit(limit);
if (skip) query = query.skip(skip);
const [data, total] = await Promise.all([
query.exec(),
this.model.countDocuments(filter)
]);
return this.createPageInfo(data, total, limit || total, skip || 0);
}
return query.exec();
}
async getOne(req: CrudRequest): Promise<T> {
const { parsed } = req;
const filter = this.buildFilter(parsed);
const entity = await this.model.findOne(filter).exec();
if (!entity) {
this.throwNotFoundException(this.model.modelName);
}
return entity;
}
async createOne(req: CrudRequest, dto: T | Partial<T>): Promise<T> {
const entity = new this.model(dto);
return entity.save();
}
async createMany(req: CrudRequest, dto: CreateManyDto<T | Partial<T>>): Promise<T[]> {
if (!dto.bulk || !dto.bulk.length) {
this.throwBadRequestException('Empty data. Nothing to save.');
}
return this.model.insertMany(dto.bulk);
}
async updateOne(req: CrudRequest, dto: T | Partial<T>): Promise<T> {
const { parsed } = req;
const filter = this.buildFilter(parsed);
const entity = await this.model.findOneAndUpdate(
filter,
{ $set: dto },
{ new: true }
).exec();
if (!entity) {
this.throwNotFoundException(this.model.modelName);
}
return entity;
}
async replaceOne(req: CrudRequest, dto: T | Partial<T>): Promise<T> {
const { parsed } = req;
const filter = this.buildFilter(parsed);
const entity = await this.model.findOneAndReplace(
filter,
dto as any,
{ new: true }
).exec();
if (!entity) {
this.throwNotFoundException(this.model.modelName);
}
return entity;
}
async deleteOne(req: CrudRequest): Promise<void | T> {
const { parsed, options } = req;
const filter = this.buildFilter(parsed);
const returnDeleted = options.routes?.deleteOneBase?.returnDeleted;
if (returnDeleted) {
const entity = await this.model.findOneAndDelete(filter).exec();
if (!entity) {
this.throwNotFoundException(this.model.modelName);
}
return entity;
}
await this.model.deleteOne(filter).exec();
}
async recoverOne(req: CrudRequest): Promise<T> {
// Implement soft delete recovery if your schema supports it
const { parsed } = req;
const filter = this.buildFilter(parsed);
const entity = await this.model.findOneAndUpdate(
filter,
{ $unset: { deletedAt: 1 } },
{ new: true }
).exec();
if (!entity) {
this.throwNotFoundException(this.model.modelName);
}
return entity;
}
private buildFilter(parsed: ParsedRequestParams): FilterQuery<T> {
const filter: FilterQuery<T> = {};
// Build filter from parsed.search
if (parsed.search) {
// Convert parsed.search to Mongoose filter
// This is a simplified example
Object.keys(parsed.search).forEach(key => {
if (key === '$and' || key === '$or') {
filter[key] = parsed.search[key];
} else {
filter[key] = parsed.search[key];
}
});
}
// Add param filters
if (parsed.paramsFilter?.length) {
parsed.paramsFilter.forEach(param => {
filter[param.field] = param.value;
});
}
return filter;
}
}
Prisma Example
Here’s an example for Prisma:import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { CrudService, CrudRequest, CreateManyDto, GetManyDefaultResponse } from '@nestjsx/crud';
@Injectable()
export class PrismaCrudService<T> extends CrudService<T> {
constructor(
private prisma: PrismaService,
private modelName: string
) {
super();
}
private get model() {
return this.prisma[this.modelName];
}
async getMany(req: CrudRequest): Promise<GetManyDefaultResponse<T> | T[]> {
const { parsed, options } = req;
const where = this.buildWhere(parsed);
const orderBy = this.buildOrderBy(parsed);
if (this.decidePagination(parsed, options)) {
const take = this.getTake(parsed, options.query);
const skip = this.getSkip(parsed, take);
const [data, total] = await Promise.all([
this.model.findMany({ where, orderBy, take, skip }),
this.model.count({ where })
]);
return this.createPageInfo(data, total, take || total, skip || 0);
}
return this.model.findMany({ where, orderBy });
}
async getOne(req: CrudRequest): Promise<T> {
const { parsed } = req;
const where = this.buildWhere(parsed);
const entity = await this.model.findFirst({ where });
if (!entity) {
this.throwNotFoundException(this.modelName);
}
return entity;
}
async createOne(req: CrudRequest, dto: T | Partial<T>): Promise<T> {
return this.model.create({ data: dto });
}
async createMany(req: CrudRequest, dto: CreateManyDto<T | Partial<T>>): Promise<T[]> {
if (!dto.bulk || !dto.bulk.length) {
this.throwBadRequestException('Empty data. Nothing to save.');
}
await this.model.createMany({ data: dto.bulk });
return dto.bulk as T[];
}
async updateOne(req: CrudRequest, dto: T | Partial<T>): Promise<T> {
const { parsed } = req;
const where = this.buildWhere(parsed);
return this.model.update({ where, data: dto });
}
async replaceOne(req: CrudRequest, dto: T | Partial<T>): Promise<T> {
// Prisma doesn't have a direct replace, so we update
return this.updateOne(req, dto);
}
async deleteOne(req: CrudRequest): Promise<void | T> {
const { parsed, options } = req;
const where = this.buildWhere(parsed);
const returnDeleted = options.routes?.deleteOneBase?.returnDeleted;
if (returnDeleted) {
return this.model.delete({ where });
}
await this.model.delete({ where });
}
async recoverOne(req: CrudRequest): Promise<T> {
const { parsed } = req;
const where = this.buildWhere(parsed);
return this.model.update({
where,
data: { deletedAt: null }
});
}
private buildWhere(parsed: any) {
const where: any = {};
// Build Prisma where clause from parsed request
if (parsed.search) {
// Convert to Prisma format
Object.assign(where, parsed.search);
}
if (parsed.paramsFilter?.length) {
parsed.paramsFilter.forEach(param => {
where[param.field] = param.value;
});
}
return where;
}
private buildOrderBy(parsed: any) {
if (!parsed.sort?.length) return undefined;
return parsed.sort.map(sort => ({
[sort.field]: sort.order.toLowerCase()
}));
}
}
REST API Backend Example
You can even create a CRUD service that communicates with a REST API:import { Injectable, HttpService } from '@nestjs/common';
import { CrudService, CrudRequest, CreateManyDto, GetManyDefaultResponse } from '@nestjsx/crud';
import { lastValueFrom } from 'rxjs';
@Injectable()
export class RestApiCrudService<T> extends CrudService<T> {
constructor(
private http: HttpService,
private baseUrl: string
) {
super();
}
async getMany(req: CrudRequest): Promise<GetManyDefaultResponse<T> | T[]> {
const { parsed, options } = req;
// Build query string from parsed parameters
const params = this.buildQueryParams(parsed);
const response = await lastValueFrom(
this.http.get<any>(`${this.baseUrl}`, { params })
);
if (this.decidePagination(parsed, options)) {
return {
data: response.data.items,
count: response.data.items.length,
total: response.data.total,
page: response.data.page,
pageCount: response.data.pageCount
};
}
return response.data;
}
async getOne(req: CrudRequest): Promise<T> {
const { parsed } = req;
const id = this.extractId(parsed);
const response = await lastValueFrom(
this.http.get<T>(`${this.baseUrl}/${id}`)
);
return response.data;
}
async createOne(req: CrudRequest, dto: T | Partial<T>): Promise<T> {
const response = await lastValueFrom(
this.http.post<T>(`${this.baseUrl}`, dto)
);
return response.data;
}
async createMany(req: CrudRequest, dto: CreateManyDto<T | Partial<T>>): Promise<T[]> {
const response = await lastValueFrom(
this.http.post<T[]>(`${this.baseUrl}/bulk`, dto.bulk)
);
return response.data;
}
async updateOne(req: CrudRequest, dto: T | Partial<T>): Promise<T> {
const { parsed } = req;
const id = this.extractId(parsed);
const response = await lastValueFrom(
this.http.patch<T>(`${this.baseUrl}/${id}`, dto)
);
return response.data;
}
async replaceOne(req: CrudRequest, dto: T | Partial<T>): Promise<T> {
const { parsed } = req;
const id = this.extractId(parsed);
const response = await lastValueFrom(
this.http.put<T>(`${this.baseUrl}/${id}`, dto)
);
return response.data;
}
async deleteOne(req: CrudRequest): Promise<void | T> {
const { parsed, options } = req;
const id = this.extractId(parsed);
const returnDeleted = options.routes?.deleteOneBase?.returnDeleted;
const response = await lastValueFrom(
this.http.delete<T>(`${this.baseUrl}/${id}`)
);
return returnDeleted ? response.data : undefined;
}
async recoverOne(req: CrudRequest): Promise<T> {
const { parsed } = req;
const id = this.extractId(parsed);
const response = await lastValueFrom(
this.http.post<T>(`${this.baseUrl}/${id}/recover`, {})
);
return response.data;
}
private buildQueryParams(parsed: any): Record<string, any> {
const params: Record<string, any> = {};
if (parsed.limit) params.limit = parsed.limit;
if (parsed.offset) params.offset = parsed.offset;
if (parsed.page) params.page = parsed.page;
if (parsed.sort?.length) {
params.sort = parsed.sort.map(s => `${s.field},${s.order}`).join(';');
}
return params;
}
private extractId(parsed: any): string | number {
if (parsed.paramsFilter?.length) {
return parsed.paramsFilter[0].value;
}
throw new Error('No ID found in request');
}
}
Using Your Custom Service
Once you’ve created your custom service, use it in your controller:import { Controller } from '@nestjs/common';
import { Crud } from '@nestjsx/crud';
import { MongoCrudService } from './mongo-crud.service';
import { User } from './user.entity';
@Crud({
model: {
type: User,
},
})
@Controller('users')
export class UsersController {
constructor(public service: MongoCrudService<User>) {}
}
Best Practices
Reuse Base Utilities: Always use the inherited utility methods like
decidePagination, getTake, getSkip, and createPageInfo for consistent behavior.Error Handling: Use
throwBadRequestException and throwNotFoundException for consistent error responses across your application.Validation: Make sure to validate input data before processing. The framework doesn’t automatically validate DTOs in custom services.
Transaction Support: If your data source supports transactions, consider implementing transaction handling in your custom service for data consistency.
Advanced Customization
Override Pagination Format
You can overridecreatePageInfo to customize the pagination response:
createPage Info(data: T[], total: number, limit: number, offset: number) {
return {
items: data,
pagination: {
total,
limit,
offset,
hasMore: offset + data.length < total
}
};
}
Add Custom Methods
Extend your service with custom methods for specific use cases:export class MongoCrudService<T> extends CrudService<T> {
// ... standard CRUD methods ...
async findByEmail(email: string): Promise<T> {
const entity = await this.model.findOne({ email }).exec();
if (!entity) {
this.throwNotFoundException('User');
}
return entity;
}
async bulkUpdate(filter: any, update: any): Promise<number> {
const result = await this.model.updateMany(filter, update).exec();
return result.modifiedCount;
}
}
See Also
- CrudService - Abstract base class documentation
- TypeOrmCrudService - Reference implementation for TypeORM