/nestjs-prisma

Easy Prisma support for your NestJS application

Primary LanguageTypeScriptMIT LicenseMIT

nestjs-prisma

Easy Prisma support for your NestJS application.

Build Status Total Downloads npm package License

Installation

Install the library:

# npm
npm install nestjs-prisma

# yarn
yarn add nestjs-prisma

or install it automatically using the schematics command:

nest add nestjs-prisma

Besides installing the library, the schematics allows to configure Prisma, Docker and even a custom PrismaService.

Basic usage

Add PrismaModule to the imports section in your AppModule or other modules to gain access to PrismaService.

import { Module } from '@nestjs/common';
import { PrismaModule } from 'nestjs-prisma';

@Module({
  imports: [PrismaModule.forRoot()],
})
export class AppModule {}

Use the PrismaService via dependency injection in your controller, resolver, services, guards and more:

import { Injectable } from '@nestjs/common';
import { PrismaService } from 'nestjs-prisma';

@Injectable()
export class AppService {
  constructor(private prisma: PrismaService) {}

  users() {
    return this.prisma.user.findMany();
  }

  user(userId: string) {
    return this.prisma.user.findUnique({
      where: { id: userId },
    });
  }
}

You have access to all exposed methods and arguments of the generated PrismaClient through PrismaService.

Shutdown Hook

Handle Prisma shutdown signal to shutdown your Nest application.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { PrismaService } from 'nestjs-prisma';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // enable shutdown hook
  const prismaService: PrismaService = app.get(PrismaService);
  prismaService.enableShutdownHooks(app);

  await app.listen(3000);
}
bootstrap();

Prisma Middleware

Apply Prisma Middlewares with PrismaModule

import { Module } from '@nestjs/common';
import { PrismaModule } from 'nestjs-prisma';

@Module({
  imports: [
    PrismaModule.forRoot({
      prismaServiceOptions: {
        middlewares: [
          async (params, next) => {
            // Before query: change params
            const result = await next(params);
            // After query: result
            return result;
          },
        ], // see example loggingMiddleware below
      },
    }),
  ],
})
export class AppModule {}

Here is an example for using a Logging middleware.

Create your Prisma Middleware and export it as a function

// src/logging-middleware.ts
import { Prisma } from '@prisma/client';

export function loggingMiddleware(): Prisma.Middleware {
  return async (params, next) => {
    const before = Date.now();

    const result = await next(params);

    const after = Date.now();

    console.log(
      `Query ${params.model}.${params.action} took ${after - before}ms`,
    );

    return result;
  };
}

Now import your middleware and add the function into the middlewares array.

import { Module } from '@nestjs/common';
import { PrismaModule } from 'nestjs-prisma';
import { loggingMiddleware } from './logging-middleware';

@Module({
  imports: [
    PrismaModule.forRoot({
      prismaServiceOptions: {
        middlewares: [loggingMiddleware()],
      },
    }),
  ],
})
export class AppModule {}

Try out the built in Logging Middleware.

Configure PrismaModule

PrismaModule allows to be used globally and to pass options to the PrismaClient.

import { Module } from '@nestjs/common';
import { PrismaModule } from 'nestjs-prisma';

@Module({
  imports: [
    PrismaModule.forRoot({
      isGlobal: true,
      prismaServiceOptions: {
        prismaOptions: { log: ['info'] },
        explicitConnect: true,
      },
    }),
  ],
})
export class AppModule {}

Additionally, PrismaModule provides a forRootAsync to pass options asynchronously. One option is to use a factory function:

import { Module } from '@nestjs/common';
import { PrismaModule } from 'nestjs-prisma';

@Module({
  imports: [
    PrismaModule.forRootAsync({
      isGlobal: true,
      useFactory: () => ({
        prismaOptions: {
          log: ['info', 'query'],
        },
        explicitConnect: false,
      }),
    }),
  ],
})
export class AppModule {}

You can inject dependencies such as ConfigModule to load options from .env files.

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PrismaModule } from 'nestjs-prisma';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    PrismaModule.forRootAsync({
      isGlobal: true,
      useFactory: async (configService: ConfigService) => {
        return {
          prismaOptions: {
            log: [configService.get('log')],
            datasources: {
              db: {
                url: configService.get('DATABASE_URL'),
              },
            },
          },
          explicitConnect: configService.get('explicit'),
        };
      },
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

Alternatively, you can use a class instead of a factory:

import { Module } from '@nestjs/common';
import { PrismaModule } from 'nestjs-prisma';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    PrismaModule.forRootAsync({
      isGlobal: true,
      useClass: PrismaConfigService,
    }),
  ],
})
export class AppModule {}

Create the PrismaConfigService and extend it with the PrismaOptionsFactory

import { Injectable } from '@nestjs/common';
import { PrismaOptionsFactory, PrismaServiceOptions } from 'nestjs-prisma';

@Injectable()
export class PrismaConfigService implements PrismaOptionsFactory {
  constructor() {
    // TODO inject any other service here like the `ConfigService`
  }

  createPrismaOptions(): PrismaServiceOptions | Promise<PrismaServiceOptions> {
    return {
      prismaOptions: {
        log: ['info', 'query'],
      },
      explicitConnect: true,
    };
  }
}

PrismaClientExceptionFilter

nestjs-prisma provides a PrismaClientExceptionFilter to catch unhandled PrismaClientKnownRequestError and returns different HttpStatus codes instead of 500 Internal server error. The exception filter supports REST (Express/Fasitfy) and GraphQL.

To use the filter you have the following two options.

  1. Instantiate the filter in your main.ts and pass the HttpAdapterHost
//src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { PrismaClientExceptionFilter } from 'nestjs-prisma';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new PrismaClientExceptionFilter(httpAdapter));

  await app.listen(3000);
}
bootstrap();

Optionally, provide your own error code mapping via the constructor:

//src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { PrismaClientExceptionFilter } from 'nestjs-prisma';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(
    new PrismaClientExceptionFilter(httpAdapter, {
      // Prisma Error Code: HTTP Status Response
      P2000: HttpStatus.BAD_REQUEST,
      P2002: HttpStatus.CONFLICT,
      P2025: HttpStatus.NOT_FOUND,
    }),
  );

  await app.listen(3000);
}
bootstrap();

See the list of Prisma CLient Errors in the Prisma docs.

This will override the default error code mapping:

Error Code  Http Status
P2000 - "The provided value for the column is too long for the column's type. Column: {column_name}" Bad Request - 400
P2002 - "Unique constraint failed on the {constraint}" Conflict - 409
P2025 - "An operation failed because it depends on one or more records that were required but not found. {cause}" Not Found - 404
  1. Use APP_FILTER token in any module
//src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { PrismaClientExceptionFilter } from 'nestjs-prisma';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: PrismaClientExceptionFilter,
    },
  ],
})
export class AppModule {}

Logging Middleware

nestjs-prisma provides a loggingMiddleware to log the query execution time.

import { Module } from '@nestjs/common';
import { PrismaModule, loggingMiddleware } from 'nestjs-prisma';

@Module({
  imports: [
    PrismaModule.forRoot({
      prismaServiceOptions: {
        middlewares: [loggingMiddleware()],
      },
    }),
  ],
})
export class AppModule {}

The default log messages are looking as follows.

Prisma Query User.findUnique took 6ms
Prisma Query User.create took 4ms
Prisma Query Product.findMany took 10ms

Customize the logging middleware by providing your own logger, logLevel and logMessage.

import { Module } from '@nestjs/common';
import { PrismaModule, loggingMiddleware, QueryInfo } from 'nestjs-prisma';

@Module({
  imports: [
    PrismaModule.forRoot({
      prismaServiceOptions: {
        middlewares: [
          loggingMiddleware({
            logger: new Logger('PrismaMiddleware'),
            logLevel: 'log', // default is `debug`
            logMessage: (query: QueryInfo) =>
              `[Prisma Query] ${query.model}.${query.action} - ${query.executionTime}ms`,
          }),
        ],
      },
    }),
  ],
})
export class AppModule {}

The customized log messages are looking as follows.

[Nest] 51348  - 29/07/2022, 10:08:41     LOG [PrismaMiddleware] [Prisma Query] User.findUnique - 4ms
[Nest] 51348  - 29/07/2022, 10:08:50     LOG [PrismaMiddleware] [Prisma Query] User.create - 6ms
[Nest] 51348  - 29/07/2022, 10:09:13     LOG [PrismaMiddleware] [Prisma Query] Product.findMany - 9ms

Change the log level from your .env file using the @nestjs/config module. Add PRISMA_QUERY_LOG_LEVEL to your .env file with one of the log levels (log, debug, warn, error).

import { Module } from '@nestjs/common';
import { PrismaModule, loggingMiddleware, QueryInfo } from 'nestjs-prisma';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    PrismaModule.forRootAsync({
      useFactory: (config: ConfigService) => {
        return {
          middlewares: [
            loggingMiddleware({
              logger: new Logger('PrismaMiddleware'),
              logLevel: config.get('PRISMA_QUERY_LOG_LEVEL'),
            }),
          ],
          prismaOptions: { log: ['warn', 'error'] },
        };
      },
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

Schematics

The schematics automatically performs additional steps to configure Prisma and Docker in your project.

  • Initialize Prisma npx prisma init --datasource-provider postgres|...
  • Add Prisma npm scripts to your package.json
  • Add seed script for Prisma
  • Generate custom PrismaService and PrismaModule (optionally)
  • Add Dockerfile and docker-compose.yml (optionally)
  • Excludes prisma directory from build via tsconfig.build.json

Example output of the schematics:

✔ Package installation in progress... ☕
Starting library setup...
? Which datasource provider do you want to use for `prisma init`? postgresql
? Do you like to Dockerize your application? (Supports postgresql and mysql) Yes
    ✅️ Added prisma@latest
    ✅️ Added @prisma/client@latest
    ✅️ Added Prisma scripts [6]
    ✅️ Added Prisma Seed script
    ✅️ Added Docker file
    ✅️ Added Docker Compose and .env
    ✅️ Add "prisma" directory to "excludes" in tsconfig.build.json
CREATE .dockerignore (42 bytes)
CREATE Dockerfile (455 bytes)
CREATE .env (642 bytes)
CREATE docker-compose.yml (497 bytes)
UPDATE package.json (2754 bytes)
UPDATE tsconfig.build.json (130 bytes)
✔ Packages installed successfully.
✔ Packages installed successfully.
    ✅️ Initialized Prisma - Datasource postgresql

Generate custom PrismaService and PrismaModule

nest add nestjs-prisma --addPrismaService

Add the flag --addPrismaService if you like to generate your own PrismaService and PrismaModule for further customizations. Add PrismaModule to the imports section in your AppModule or other modules to gain access to PrismaService.

import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma/prisma.module';

@Module({
  imports: [PrismaModule],
})
export class AppModule {}

Note: It is safe to remove nestjs-prisma as dependency otherwise you have two import suggestions for PrismaService and PrismaModule.

Schematic options

All available options to passe to the schematic command:

Flag Description Type Default
datasourceProvider Specifies the datasource provider for prisma init and docker. boolean Prompted
addDocker Create a Dockerfile and docker-compose.yaml. boolean Prompted
addPrismaService Create a Prisma service extending the Prisma Client and module. boolean false
dockerNodeImageVersion Node version for the builder and runner image. string 14
name The name for the Prisma service extending the Prisma Client and module. string Prisma
prismaVersion The Prisma version to be installed. string latest
skipInstall Skip installing dependency packages. boolean false
skipPrismaInit Skip initializing Prisma. boolean false

You can pass additional flags to customize the schematic. For example, if you want to install a different version for Prisma use --prismaVersion flag:

nest add nestjs-prisma --prismaVersion 3.2.1

If you want to skip installing dependencies use --skipInstall flag:

nest add nestjs-prisma --skipInstall

Add Dockerfile and docker-compose.yaml, you can even use a different node version (14-alpine or 16).

Currently uses PostgreSQL as a default database in docker-compose.yaml.

nest add nestjs-prisma --addDocker --dockerNodeImageVersion 14-alpine

Contributing

You are welcome to contribute to this project.

The code is split up into two directories:

+-- lib
+-- schematics

The lib directory contains everything exposed by nestjs-prisma as a library. The schematics directory contains the blue prints for installing the library with the schematic command.

Here are some tips if you like to make changes to the schematics.

Install @angular-devkit/schematics-cli to be able to use schematics command

npm i -g @angular-devkit/schematics-cli

Now build the schematics and run the schematic.

npm run build:schematics
# or
npm run dev:schematics

# dry-run
schematics .:nest-add

# execute schematics
schematics .:nest-add --debug false
# or
schematics .:nest-add --dry-run false

Helpful article about Custom Angular Schematics which also applies to Nest.