Skip to main content

CRUD Endpoints

Schematics#

We recommend using the schematics package to quickly scaffold your CRUD modules:

  1. Install nestjs-prisma-crud-schematics globally:

    npm i nestjs-prisma-crud-schematics --save-dev
  2. Scaffold the CRUD module and endpoints (replace post with your model's name):

    nest g -c nestjs-prisma-crud-schematics crud-resource post

The above will scaffold the entire CRUD module for you, most notably:

  • post.controller.ts where you can add, remove or extend your controllers' functionality.
  • post.service.ts where you can configure your crud endpoints.

CRUD Controller#

The CRUD controller is just a regular NestJS controller with a few characteristics:

  • All routes use the generated <ModelName>Service for performing the CRUD operations.
  • All routes retrieve @Query('crudQuery') crudQuery: string and pass it along to the service.
tip

The schematic generates this file for you.

post.controller.ts
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';import { PostService } from './post.service';import { CreatePostDto } from './dto/create-post.dto';import { UpdatePostDto } from './dto/update-post.dto';
@Controller('post')export class PostController {    constructor(private readonly postService: PostService) {}
    @Post()    async create(@Body() createPostDto: CreatePostDto, @Query('crudQuery') crudQuery: string) {        const created = await this.postService.create(createPostDto, { crudQuery });        return created;    }
    @Get()    async findMany(@Query('crudQuery') crudQuery: string) {        const matches = await this.postService.findMany({ crudQuery });        return matches;    }
    @Get(':id')    async findOne(@Param('id') id: string, @Query('crudQuery') crudQuery: string) {        const match = await this.postService.findOne(id, { crudQuery });        return match;    }
    @Patch(':id')    async update(        @Param('id') id: string,        @Body() updatePostDto: UpdatePostDto,        @Query('crudQuery') crudQuery: string,    ) {        const updated = await this.postService.update(id, updatePostDto, { crudQuery });        return updated;    }
    @Delete(':id')    async remove(@Param('id') id: string, @Query('crudQuery') crudQuery: string) {        return this.postService.remove(id, { crudQuery });    }}

CRUD Service#

tip

The schematic generates this file for you.

post.service.ts
import { Injectable } from '@nestjs/common';import { PrismaCrudService } from 'nestjs-prisma-crud';
@Injectable()export class PostService extends PrismaCrudService {    constructor() {        super({            model: 'post',            allowedJoins: [],        });    }}

The configuration of your crud endpoints is defined in the super() call:

export interface CrudServiceOpts {    model: string;    prismaClient?: PrismaClient;    allowedJoins: string[];    defaultJoins?: string[];    forbiddenPaths?: Array<string | RegExp>;    idPropertyName?: string;    paginationConfig?: PaginationConfig;}

Below you can find a description of each option.

opts.model#

Type: string
Mandatory: Yes
Description:

The prismaClient.model on which you wish to perform the CRUD operations.

Example: 'post'


opts.prismaClient#

Type: PrismaClient | PrismaService
Mandatory: No
Description:

Set this value if for some reason want to use a different PrismaService from the globally provided one.

For most use cases it can be left blank.

Example: prismaService


opts.allowedJoins#

Type: Array<string>
Mandatory: No
Description:

The relations which clients can ask to include in responses (see client side usage).
Supports dot notation.

Example: ['comments.author.posts']
Default: []


opts.defaultJoins#

Type: Array<string>
Mandatory: No
Description:

The default relations to be included in responses.
Note: Paths must be shallower or same depth as provided in allowedJoins

Example: ['comments.author'] or [];
Default: opts.allowedJoins


opts.forbiddenPaths#

Type: Array<string | RegExp>
Mandatory: No
Description:

The paths you wish to omit in the returned objects.
Important: These values still get fetched from the database, and are excluded just before the function returns!!

Example:

[    'some.nested.exact.string.path',
    // RegExp: delete anything containing the word password    /.*password.*/,
    // RegExp: \d+ targets all comments in an array, deleting their .somethingSecret    /comments\.\d+\.somethingSecret/,];

Default: []


opts.idPropertyName#

Type: string
Mandatory: No
Description:

The name of the model's primary key.

Example: uuid
Default: id


opts.paginationConfig#

Type: PaginationConfig

export type PaginationConfig = {    defaultPageSize?: number;    maxPageSize?: number;    defaultOrderBy?: { [key: string]: 'asc' | 'desc' }[];};

Mandatory: No
Description:

defaultPageSize: when clients do not specify a pageSize, this option will be used.
maxPageSize: client's pageSize option will be capped at this value.
defaultOrderBy: when clients do not specify a sorting field, this option will be used by default.

Default:

const PAGINATION_DEFAULTS: PaginationConfig = {    defaultPageSize: 25,    maxPageSize: 100,    defaultOrderBy: [{ [this.idPropertyName]: 'asc' }],};

Extending Controller Functionality with Transaction Support#

info

Transaction support relies on prisma's Interactive Transactions which are currently a preview feature.

In order to use this example, you must add the following to your prisma schema:

schema.prisma
generator client {  provider        = "prisma-client-js"  previewFeatures = ["interactiveTransactions"]}

There are times when we want to extend a CRUD controller's functionality and perform additional database operations. In those cases we usually want all database operations to happen atomically (ie. if one database operation fails, cancel all other operations and leave the database unchanged).

Example#

Suppose you have a SalesController where, aside from the CRUD sale operations, you also wish to increment and decrement the balance of the users involved.

The example below achieves atomicity by following the following steps:

  1. Start a prisma interactive transaction.
  2. Pass prismaTransaction into the PrismaCrudService methods.
  3. Use the prismaTransaction instead of prismaClient for performing your database operations.
sales.controller.ts
interface CreateSaleDTO {    itemId: string;    sellerId: string;    buyerId: string;    dollarAmt: number;}
@Controller('sales')export class SalesController {    constructor(        private readonly salesService: SalesService,        private readonly prismaService: PrismaService,    ) {}
    @Post()    async createSale(@Body() createSaleDto: CreateSaleDTO, @Query('crudQuery') crudQuery: string) {        let createdSale;        // 0. Start the interactive transaction        await this.prismaService.$transaction(async (prismaTransaction) => {            // 1. create the sale record            createdSale = await this.salesService.create(createSaleDto, {                crudQuery,                prismaTransaction, //  pass prisma transaction into PrismaCrudService            });
            // 2. increment seller's ballance            // NOTE: using `prismaTransaction` instead of `this.prismaService`            await prismaTransaction.user.update({                data: {                    balance: {                        increment: createSaleDto.dollarAmt,                    },                },                where: {                    id: createSaleDto.sellerId,                },            });
            // 3. decrement buyer's ballance            // NOTE: using `prismaTransaction` instead of `this.prismaService`            await prismaTransaction.user.update({                data: {                    balance: {                        decrement: createSaleDto.dollarAmt,                    },                },                where: {                    id: createSaleDto.buyerId,                },            });        });
        return createdSale;    }}