Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nestjsx/crud/llms.txt

Use this file to discover all available pages before exploring further.

Overview

While the framework provides TypeOrmCrudService 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 abstract CrudService 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 override createPageInfo 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

Build docs developers (and LLMs) love