CRUDzilla | Simplify NestJS Development with Automated CRUD Operations
If you have been working on Web Development projects you must have realised a pattern that keeps repeating within different projects and even within the same project. We define entities, create REST APIs and do business operations to keep the app running. I realized this long back and started creating base classes for services which will take care of basic operations like createOne, updateOne, findOneByID, findAll, findManyPaginated, deleteOneByID. But recently I found even this approach troublesome as most projects are now scaled over multiple micro services and any change improvement to the base class needed to be copied over to all projects. And since basic CRUD for any entity is the most common requirement for all my projects I created this npm package called nest-crudzilla (currently for NestJS and mongo) to facilitate the CRUD out of the box.
RESTful API naming convention documented beautifully here talks about how nouns are good and verbs are bad while creating routes. If you are aligned on the basic structure of creating routes let's take an example. We have an entity called product. So we will create a controller called products, and then we will create following routes
/**
Create one prodcut
* 1.POST /api/products
Get all products
* 2. GET /api/products
Get product by ID
* 3. GET /api/products/:id
Update partial product fields by ID
* 4. PATCH /api/products/:id
Upate all product entries by ID
* 5. PUT /api/products/:id
Delete one product by ID
* 6. DELETE /api/products/:id
/*
Let's implement it in a NestJS project , let's create our products.controller.ts
import { Controller,Get,Post, Delete, Body, Patch, Param, UseGuards,} from '@nestjs/common';
import { GenerateORMFilter, ORMFilter, PaginationOption, PaginationQuery} from '@kartikyathakur/nestjs-query-filter';
import { AuthGuard } from 'src/guards/auth.guard';
import mongoose from 'mongoose';
import { ProductsService } from './products.service';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';
@Controller('products')
@UseGuards(AuthGuard)
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
// Controller endpoint to create a new product using the provided data
/**
* Create a new product with the provided data.
*
* @param createProductDto Data for creating the product.
* @returns The created product.
*/
@Post()
create(@Body() createProductDto: CreateProductDto) {
return this.productsService.createOne(createProductDto);
}
// Controller endpoint to retrieve a list of products based on provided filters and pagination options
/**
* Retrieve a paginated list of products based on provided filters and pagination options.
*
* @param ormFilter Filter options generated from the query parameters.
* @param paginationQuery Pagination options generated from the query parameters.
* @returns A paginated list of products.
*/
@Get()
findAll(
@GenerateORMFilter() ormFilter: ORMFilter,
@PaginationOption() paginationQuery: PaginationQuery,
) {
return this.productsService.findManyPaginated(ormFilter, paginationQuery);
}
// Controller endpoint to find a product by its unique identifier
/**
* Find a product by its unique identifier.
*
* @param productId The unique identifier of the product.
* @returns The found product.
*/
@Get(':productId')
findByProductId(
@Param('productId') productId: mongoose.Types.ObjectId,
) {
return this.productsService.findOneById(productId);
}
// Controller endpoint to update an existing product with the provided data
/**
* Update an existing product with the provided data.
*
* @param productId The unique identifier of the product to update.
* @param updateProductDto Data for updating the product.
* @returns The updated product.
*/
@Patch(':productId')
update(
@Param('productId') productId: mongoose.Types.ObjectId,
@Body() updateProductDto: UpdateProductDto
) {
return this.productsService.updateOneById(productId, updateProductDto);
}
// Controller endpoint to delete a product by its unique identifier
/**
* Delete a product by its unique identifier.
*
* @param productId The unique identifier of the product to delete.
* @returns A message indicating the deletion of the product.
*/
@Delete(':productId')
remove(
@Param('productId') productId: mongoose.Types.ObjectId
) {
return this.productsService.deleteOneById(productId);
}
}
Now lets create our products.service.ts
the service to implement the mongoose ORM implementation for this controller
import { Injectable, ConflictException, NotFoundException} from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Coupon } from '/models/products.schema';
import { ConnectionName } from 'src/utils/connectionName';
import { Model } from 'mongoose';
import mongoose from 'mongoose';
import { CouponDetailsDto } from './dto/coupon-details.dto';
@Injectable()
export class CouponsService extends Coupon {
constructor(
@InjectModel('Coupon', ConnectionName.DB)
private readonly couponModel: Model<Coupon>,
) {
super(couponModel);
}
async createOne(document: Partial<any>) {
const newDocument = new this.couponModel(document);
try {
return await newDocument.save();
} catch (e) {
throw new ConflictException(e.message);
}
}
async createMany(documents) {
return await this.couponModel.insertMany(documents);
}
async findOne(query: Partial<any>) {
return await this.couponModel.findOne(query).exec();
}
async findOneById(id: mongoose.Types.ObjectId, projection?: any, populate?:{path: string, select: string}) {
let existingDocument
if(populate) {
existingDocument = await this.couponModel.findById(id, projection).populate(populate.path, populate.select).exec();
} else {
existingDocument = await this.couponModel.findById(id, projection).exec();
}
if (!existingDocument) {
throw new NotFoundException('Not Found');
}
return existingDocument;
}
async findMany(query: Partial<any>) {
return await this.couponModel.find(query).sort({createdAt:-1}).exec();
}
async findManyPaginated(ormFilter?: Partial<any>, paginationQuery?: Partial<any>, projection?: Partial<any>, populate?:{path: string, select: string}) {
let modelQuery
if(populate) {
modelQuery = await this.couponModel.find(ormFilter, projection, paginationQuery)
.lean().populate(populate.path, populate.select)
} else {
modelQuery = await this.couponModel.find(ormFilter, projection, paginationQuery)
.lean()
}
const countQuery = ormFilter ? this.couponModel.countDocuments(ormFilter) : this.couponModel.estimatedDocumentCount();
const [data, count] = await Promise.all([modelQuery, countQuery])
return paginationQuery.generatePaginatedResponse(count, data);
}
async updateOneById(id: mongoose.Types.ObjectId, update: Partial<any>) {
const updateResult = await this.couponModel
.updateOne({ _id: id }, update)
.exec();
if (updateResult.matchedCount === 0) {
throw new ConflictException(`Not Found, Can't Update`);
}
return updateResult;
}
async replaceOneById(id: mongoose.Types.ObjectId, replacement: Partial<any>) {
const updateResult = await this.couponModel
.replaceOne({ _id: id }, replacement)
.exec();
if (updateResult.matchedCount === 0) {
throw new ConflictException(`Not Found, Can't Update`);
}
return updateResult;
}
async findOneAndUpdate(query: Partial<any>, update: Partial<any>) {
return await this.couponModel.findOneAndUpdate(query, update).exec();
}
async updateManyByIds(ids: mongoose.Types.ObjectId[], update: Partial<any>) {
return await this.couponModel.updateMany({ _id: { $in: ids } }, update).exec();
}
async deleteOne(query: Partial<any>) {
return await this.couponModel.deleteOne(query).exec();
}
async deleteOneById(id: mongoose.Types.ObjectId) {
const deleteResult = await this.couponModel.deleteOne({ _id: id }).exec();
if (deleteResult.deletedCount === 0) {
throw new ConflictException(`Not Found, Can't Delete`);
}
return deleteResult;
}
async deleteMany(query) {
return await this.couponModel.deleteMany(query).exec();
}
}
All the controllers and services implemented for Product here are generic and will be reused for most of the other entities that we develop for the project. Now if we implement my npm package @vishivish18/nest-crudzilla
we can get rid of all this part from all our controllers and extend CrudController
from the package.
import { Controller,Get,Post, Delete, Body, Patch, Param, UseGuards,} from '@nestjs/common';
import { ProductsService } from './products.service';
import { Product } from './models/products.schema';
import { CreateProductDto } from './dto/create-product.dto';
import { CrudController } from '@vishivish18/nest-crudzilla';
@Controller('products')
export class CouponsController extends CrudController<Product> {
constructor(
private readonly productsService: ProductsService
) {
super(productsService, CreateProductDto);
}
}
For the Service also the new service will look leaner when we extend CrudService
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Product } from './models/products.schema';
import { ConnectionName } from 'src/utils/connectionName';
import { Model } from 'mongoose';
import { CrudService} from '@vishivish18/nest-crudzilla';
@Injectable()
export class ProductsService extends CrudService {
constructor(
@InjectModel('Product', ConnectionName.DB)
private readonly productModel: Model<Product>,
) {
super(productModel);
}
}
As you can see we can get rid of a lot of repetitive code by using this. I am looking forward to add more functions to this and add support for Type ORM. Maybe even a pip package for my python 🐍 projects.
npm install @vishivish18/nest-crudzilla