diff --git a/apps/blog/content/blog/nestjs-prisma-authentication-7D056s1s0k3l/index.mdx b/apps/blog/content/blog/nestjs-prisma-authentication-7D056s1s0k3l/index.mdx index 35f39611ce..e8af2c3e69 100644 --- a/apps/blog/content/blog/nestjs-prisma-authentication-7D056s1s0k3l/index.mdx +++ b/apps/blog/content/blog/nestjs-prisma-authentication-7D056s1s0k3l/index.mdx @@ -2,10 +2,11 @@ title: "Building a REST API with NestJS and Prisma: Authentication" slug: "nestjs-prisma-authentication-7D056s1s0k3l" date: "2023-03-31" +updatedAt: "2026-07-08" authors: - "Tasin Ishmam" -metaTitle: "Building a REST API with NestJS and Prisma: Authentication" -metaDescription: "In this tutorial, you will learn how to implement JWT authentication with NestJS, Prisma and PostgreSQL. You will also learn about salting passwords, security best practises and how to integrate with Swagger." +metaTitle: "JWT Authentication in a REST API with NestJS and Prisma 7" +metaDescription: "Learn how to implement JWT authentication in a NestJS REST API with Prisma ORM 7 and Passport: login endpoint, JWT strategy, auth guards, Swagger integration and bcrypt password hashing." metaImagePath: "/nestjs-prisma-authentication-7D056s1s0k3l/imgs/meta-425dd76be3dd7fa36e7051613296031be0344159-1920x1080.png" heroImagePath: "/nestjs-prisma-authentication-7D056s1s0k3l/imgs/hero-7c1df9404c5bc6f5047c3e3ba1972005f90ea2bb-843x474.svg" heroImageAlt: "Building a REST API with NestJS and Prisma: Authentication" @@ -19,23 +20,22 @@ tags: 10 min read - Welcome to the fifth tutorial in this series about building a REST API with NestJS, Prisma and PostgreSQL! - In this tutorial, you will learn how to implement JWT authentication in your NestJS REST API. + Welcome to the fifth and final tutorial in this series about building a REST API with NestJS and Prisma! In this tutorial, you will add JWT authentication to the API with Passport: a login endpoint that issues tokens, a JWT strategy and guard that protect routes, Swagger integration, and password hashing with bcrypt. -> **Update (May 2026):** This tutorial is still useful for the authentication flow, but if you're starting fresh you should pair it with the current [NestJS + Prisma guide](https://www.prisma.io/docs/guides/frameworks/nestjs), [Prisma ORM](https://www.prisma.io/orm), and [Prisma Postgres](https://www.prisma.io/postgres) or your existing PostgreSQL database. +> **Updated (July 2026):** This tutorial has been fully revised for **Prisma ORM 7** and **NestJS 11**, and continues from the updated [fourth part](/nestjs-prisma-relational-data-7D056s1kOabc) of the series. Every command and code block below was run end-to-end against `prisma@7.8`, `@prisma/client@7.8`, `@nestjs/core@11`, `@nestjs/passport@11` and `bcrypt@6`, with the full login flow verified against a live [Prisma Postgres](https://www.prisma.io/postgres) database. ## Table Of Contents - [Introduction](#introduction) - * [Development environment](#development-environment) - * [Clone the repository](#clone-the-repository) - * [Project structure and files](#project-structure-and-files) + - [Development environment](#development-environment) + - [Set up the project](#set-up-the-project) + - [Project structure and files](#project-structure-and-files) - [Implement authentication in your REST API](#implement-authentication-in-your-rest-api) - * [Install and configure `passport`](#install-and-configure-passport) - * [Implement a `POST /auth/login` endpoint](#implement-a-post-authlogin-endpoint) - * [Implement JWT authentication strategy](#implement-jwt-authentication-strategy) - * [Implement JWT auth guard](#implement-jwt-auth-guard) - * [Integrate authentication in Swagger](#integrate-authentication-in-swagger) + - [Install and configure `passport`](#install-and-configure-passport) + - [Implement a `POST /auth/login` endpoint](#implement-a-post-authlogin-endpoint) + - [Implement JWT authentication strategy](#implement-jwt-authentication-strategy) + - [Implement JWT auth guard](#implement-jwt-auth-guard) + - [Integrate authentication in Swagger](#integrate-authentication-in-swagger) - [Hashing passwords](#hashing-passwords) - [Summary and final remarks](#summary-and-final-remarks) @@ -43,109 +43,86 @@ tags: In the [previous chapter](/nestjs-prisma-relational-data-7D056s1kOabc) of this series, you learned how to handle relational data in your NestJS REST API. You created a `User` model and added a one-to-many relationship between `User` and `Article` models. You also implemented the CRUD endpoints for the `User` model. -In this chapter, you will learn how to add authentication to your API using a package called [Passport](https://www.npmjs.com/package/passport): +In this chapter, you will learn how to add authentication to your API: 1. First, you will implement JSON Web Token (JWT) based authentication using a library called [Passport](https://www.npmjs.com/package/passport). 2. Next, you will protect the passwords stored in your database by hashing them using the [bcrypt](https://www.npmjs.com/package/bcrypt) library. - -In this tutorial, you will use the API built in the [last chapter](/nestjs-prisma-relational-data-7D056s1kOabc). - - +This tutorial continues from the end of the [fourth chapter](/nestjs-prisma-relational-data-7D056s1kOabc). ### Development environment To follow along with this tutorial, you will be expected to: -- ... have [Node.js](https://nodejs.org) installed. -- ... have [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/install/#compose-installation-scenarios) installed. If you are using Linux, please make sure your Docker version is 20.10.0 or higher. You can check your Docker version by running `docker version` in the terminal. -- ... _optionally_ have the [Prisma VS Code Extension](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma) installed. The Prisma VS Code extension adds some really nice IntelliSense and syntax highlighting for Prisma. -- ... _optionally_ have access to a Unix shell (like the terminal/shell in Linux and macOS) to run the commands provided in this series. +- ... have [Node.js](https://nodejs.org) (v18 or higher) installed. +- ... have a PostgreSQL database. The easiest option is [Prisma Postgres](https://www.prisma.io/postgres); you can also run Postgres locally with [Docker](https://www.docker.com/) or a native install. +- ... have the [Prisma VSCode Extension](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma) installed. _(optional)_ +- ... have access to a Unix shell (like the terminal/shell in Linux and macOS) to run the commands provided in this series. _(optional)_ -If you don't have a Unix shell (for example, you are on a Windows machine), you can still follow along, but the shell commands may need to be modified for your machine. +> **Note**: If you don't have a Unix shell (for example, you are on a Windows machine), you can still follow along, but the shell commands may need to be modified for your machine. -### Clone the repository +### Set up the project -The starting point for this tutorial is the ending of [chapter two](/nestjs-prisma-validation-7D056s1kOla1) of this series. It contains a rudimentary REST API built with NestJS. +The starting point for this tutorial is the ending of [part four](/nestjs-prisma-relational-data-7D056s1kOabc) of this series: the Median REST API with users, articles and their relation in place. If you're jumping in here, work through the earlier parts first; each one builds directly on the previous. -The starting point for this tutorial is available in the [`end-validation`](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma/tree/end-validation) branch of the [GitHub repository](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma). To get started, clone the repository and checkout the `end-validation` branch: +Before continuing, make sure the project runs: + +1. Start the development server: ```shell -git clone -b end-relational-data git@github.com:prisma/blog-backend-rest-api-nestjs-prisma.git +npm run start:dev ``` -Now, perform the following actions to get started: - -1. Navigate to the cloned directory: - ```shell - cd blog-backend-rest-api-nestjs-prisma - ``` -2. Install dependencies: - ```shell - npm install - ``` -3. Start the PostgreSQL database with Docker: - ```shell - docker-compose up -d - ``` -4. Apply database migrations: - ```shell - npx prisma migrate dev - ``` -5. Start the project: - ```shell - npm run start:dev - ``` - -> **Note**: Step 4 will also generate Prisma Client and seed the database. - -Now, you should be able to access the API documentation at [`http://localhost:3000/api/`](http://localhost:3000/api/). + +2. Confirm the API documentation is available at [`http://localhost:3000/api/`](http://localhost:3000/api/). + +> **Note**: The original edition of this series linked to a companion GitHub repository. That repository still targets the older Prisma 4 and NestJS 8 stack, so for the updated series you should continue from your own project. ### Project structure and files -The repository you cloned should have the following structure: +Your project should have the following structure: + ``` median ├── node_modules + ├── generated + │ └── prisma ├── prisma │ ├── migrations │ ├── schema.prisma │ └── seed.ts ├── src - │ ├── app.controller.spec.ts - │ ├── app.controller.ts │ ├── app.module.ts - │ ├── app.service.ts │ ├── main.ts │ ├── articles │ ├── users - │ └── prisma + │ ├── prisma + │ └── prisma-client-exception ├── test - │   ├── app.e2e-spec.ts - │   └── jest-e2e.json + │ ├── app.e2e-spec.ts + │ └── jest-e2e.json ├── README.md ├── .env - ├── docker-compose.yml + ├── eslint.config.mjs ├── nest-cli.json ├── package-lock.json ├── package.json + ├── prisma.config.ts ├── tsconfig.build.json └── tsconfig.json ``` -> **Note**: You might notice that this folder comes with a `test` directory as well. Testing won't be covered in this tutorial. However, if you want to learn about how best practices for testing your applications with Prisma, be sure to check out this tutorial series: [The Ultimate Guide to Testing with Prisma](https://www.prisma.io/blog/series/testing-with-prisma) + +> **Note**: You might notice that this folder comes with a `test` directory as well. Testing won't be covered in this tutorial. However, if you want to learn about best practices for testing your applications with Prisma, be sure to check out this tutorial series: [The Ultimate Guide to Testing with Prisma](https://www.prisma.io/blog/series/testing-with-prisma) The notable files and directories in this repository are: -- The `src` directory contains the source code for the application. There are three modules: +- The `src` directory contains the source code for the application. There are four feature modules: - The `app` module is situated in the root of the `src` directory and is the entry point of the application. It is responsible for starting the web server. - - The `prisma` module contains Prisma Client, your interface to the database. + - The `prisma` module contains `PrismaService`, your interface to the database. - The `articles` module defines the endpoints for the `/articles` route and accompanying business logic. - The `users` module defines the endpoints for the `/users` route and accompanying business logic. -- The `prisma` folder has the following: - - The `schema.prisma` file defines the database schema. - - The `migrations` directory contains the database migration history. - - The `seed.ts` file contains a script to seed your development database with dummy data. -- The `docker-compose.yml` file defines the Docker image for your PostgreSQL database. -- The `.env` file contains the database connection string for your PostgreSQL database. +- The `prisma` directory contains the `schema.prisma` file (database schema), the `migrations` directory (migration history) and the `seed.ts` script. +- The `generated/prisma` directory contains the generated Prisma Client, which in Prisma 7 lives in your project instead of `node_modules`. +- The `prisma.config.ts` file is Prisma's central configuration file, and `.env` contains the `DATABASE_URL` connection string. > **Note**: For more information about these components, go through [chapter one](/nestjs-prisma-rest-api-7D056s1BmOL0) of this tutorial series. @@ -159,16 +136,14 @@ In this section, you will implement the bulk of the authentication logic for you - `PATCH /users/:id` - `DELETE /users/:id` - There are two main types of authentication used on the web: _session-based_ authentication and _token-based_ authentication. In this tutorial, you will implement token-based authentication using [JSON Web Tokens (JWT)](https://jwt.io/). -> **Note**: [This short video](https://youtu.be/UBUNrFtufWo) explains the basics of both kinds of authentication. - -To get started, create a new `auth` module in your application. Run the following command to generate a new module: +To get started, create a new `auth` module in your application. Run the following command to generate a new resource: ```shell npx nest generate resource ``` + You will be given a few CLI prompts. Answer the questions accordingly: 1. `What name would you like to use for this resource (plural, e.g., "users")?` **auth** @@ -184,36 +159,39 @@ You should now find a new `auth` module in the `src/auth` directory. Get started by installing the following packages: ```shell -npm install --save @nestjs/passport passport @nestjs/jwt passport-jwt +npm install @nestjs/passport passport @nestjs/jwt passport-jwt npm install --save-dev @types/passport-jwt ``` -Now that you have installed the required packages, you can configure `passport` in your application. Open the `src/auth.module.ts` file and add the following code: + +Now that you have installed the required packages, you can configure `passport` in your application. Open the `src/auth/auth.module.ts` file and update it with the following code: ```ts -//src/auth/auth.module.ts +// src/auth/auth.module.ts + import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; import { AuthController } from './auth.controller'; -+import { PassportModule } from '@nestjs/passport'; -+import { JwtModule } from '@nestjs/jwt'; -+import { PrismaModule } from 'src/prisma/prisma.module'; +import { PassportModule } from '@nestjs/passport'; +import { JwtModule } from '@nestjs/jwt'; +import { PrismaModule } from '../prisma/prisma.module'; -+export const jwtSecret = 'zjP9h6ZI5LoSKCRj'; +export const jwtSecret = 'zjP9h6ZI5LoSKCRj'; @Module({ -+ imports: [ -+ PrismaModule, -+ PassportModule, -+ JwtModule.register({ -+ secret: jwtSecret, -+ signOptions: { expiresIn: '5m' }, // e.g. 30s, 7d, 24h -+ }), -+ ], + imports: [ + PrismaModule, + PassportModule, + JwtModule.register({ + secret: jwtSecret, + signOptions: { expiresIn: '5m' }, // e.g. 30s, 7d, 24h + }), + ], controllers: [AuthController], providers: [AuthService], }) export class AuthModule {} ``` + The `@nestjs/passport` module provides a `PassportModule` that you can import into your application. The `PassportModule` is a wrapper around the `passport` library that provides NestJS specific utilities. You can read more about the `PassportModule` in the [official documentation](https://docs.nestjs.com/recipes/passport). You also configured a `JwtModule` that you will use to generate and verify JWTs. The `JwtModule` is a wrapper around the [`jsonwebtoken`](https://www.npmjs.com/package/jsonwebtoken) library. The `secret` provides a secret key that is used to sign the JWTs. The `expiresIn` object defines the expiration time of the JWTs. It is currently set to 5 minutes. @@ -222,11 +200,11 @@ You also configured a `JwtModule` that you will use to generate and verify JWTs. You can use the `jwtSecret` shown in the code snippet or generate your own using OpenSSL. -> **Note**: In a real application, you should never store the secret directly in your codebase. NestJS provides the `@nestjs/config` package for loading secrets from environment variables. You can read more about it in the [official documentation](https://docs.nestjs.com/techniques/configuration). +> **Note**: In a real application, you should never store the secret directly in your codebase. NestJS provides the `@nestjs/config` package for loading secrets from environment variables; you already registered its `ConfigModule` in part one to load `DATABASE_URL`. You can read more about it in the [official documentation](https://docs.nestjs.com/techniques/configuration). ### Implement a `POST /auth/login` endpoint -The `POST /login` endpoint will be used to authenticate users. It will accept a username and password and return a JWT if the credentials are valid. First you create a `LoginDto` class that will define the shape of the request body. +The `POST /login` endpoint will be used to authenticate users. It will accept an email and password and return a JWT if the credentials are valid. First you create a `LoginDto` class that will define the shape of the request body. Create a new file called `login.dto.ts` inside the `src/auth/dto` directory: @@ -234,10 +212,12 @@ Create a new file called `login.dto.ts` inside the `src/auth/dto` directory: mkdir src/auth/dto touch src/auth/dto/login.dto.ts ``` -Now define the `LoginDto` class with a `email` and `password` field: + +Now define the `LoginDto` class with an `email` and `password` field: ```ts -//src/auth/dto/login.dto.ts +// src/auth/dto/login.dto.ts + import { ApiProperty } from '@nestjs/swagger'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; @@ -254,16 +234,19 @@ export class LoginDto { password: string; } ``` -You will also need to define a new `AuthEntity` that will describe the shape of the JWT payload. Create a new file called `auth.entity.ts` inside the `src/auth/entity` directory: + +You will also need to define a new `AuthEntity` that will describe the shape of the response. Create a new file called `auth.entity.ts` inside the `src/auth/entity` directory: ```shell mkdir src/auth/entity touch src/auth/entity/auth.entity.ts ``` + Now define the `AuthEntity` in this file: ```ts -//src/auth/entity/auth.entity.ts +// src/auth/entity/auth.entity.ts + import { ApiProperty } from '@nestjs/swagger'; export class AuthEntity { @@ -271,28 +254,33 @@ export class AuthEntity { accessToken: string; } ``` + The `AuthEntity` just has a single string field called `accessToken`, which will contain the JWT. Now create a new `login` method inside `AuthService`: ```ts -//src/auth/auth.service.ts +// src/auth/auth.service.ts + import { Injectable, NotFoundException, UnauthorizedException, } from '@nestjs/common'; -import { PrismaService } from './../prisma/prisma.service'; +import { PrismaService } from '../prisma/prisma.service'; import { JwtService } from '@nestjs/jwt'; import { AuthEntity } from './entity/auth.entity'; @Injectable() export class AuthService { - constructor(private prisma: PrismaService, private jwtService: JwtService) {} + constructor( + private prisma: PrismaService, + private jwtService: JwtService, + ) {} async login(email: string, password: string): Promise { // Step 1: Fetch a user with the given email - const user = await this.prisma.user.findUnique({ where: { email: email } }); + const user = await this.prisma.user.findUnique({ where: { email } }); // If no user is found, throw an error if (!user) { @@ -314,34 +302,38 @@ export class AuthService { } } ``` -The `login` method first fetches a user with the given email. If no user is found, it throws a `NotFoundException`. If a user is found, it checks if the password is correct. If the password is incorrect, it throws a `UnauthorizedException`. If the password is correct, it generates a JWT containing the user's ID and returns it. -Now create the `POST /auth/login` method inside `AuthController`: +The `login` method first fetches a user with the given email. If no user is found, it throws a `NotFoundException`. If a user is found, it checks if the password is correct. If the password is incorrect, it throws an `UnauthorizedException`. If the password is correct, it generates a JWT containing the user's ID and returns it. + +> **Note**: Comparing passwords in plain text like this is temporary. You will replace this check with proper password hashing in the [Hashing passwords](#hashing-passwords) section. + +Now create the `POST /auth/login` method inside `AuthController`: ```ts -//src/auth/auth.controller.ts +// src/auth/auth.controller.ts -+import { Body, Controller, Post } from '@nestjs/common'; +import { Body, Controller, Post } from '@nestjs/common'; import { AuthService } from './auth.service'; -+import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; -+import { AuthEntity } from './entity/auth.entity'; -+import { LoginDto } from './dto/login.dto'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { AuthEntity } from './entity/auth.entity'; +import { LoginDto } from './dto/login.dto'; @Controller('auth') -+@ApiTags('auth') +@ApiTags('auth') export class AuthController { constructor(private readonly authService: AuthService) {} -+ @Post('login') -+ @ApiOkResponse({ type: AuthEntity }) -+ login(@Body() { email, password }: LoginDto) { -+ return this.authService.login(email, password); -+ } + @Post('login') + @ApiOkResponse({ type: AuthEntity }) + login(@Body() { email, password }: LoginDto) { + return this.authService.login(email, password); + } } ``` + Now you should have a new `POST /auth/login` endpoint in your API. -Go to the [`http://localhost:3000/api`](http://localhost:3000/api) page and try the `POST /auth/login` endpoint. Provide the credentials of a user that you created in your seed script +Go to the [`http://localhost:3000/api`](http://localhost:3000/api) page and try the `POST /auth/login` endpoint. Provide the credentials of a user that you created in your seed script. You can use the following request body: @@ -351,13 +343,13 @@ You can use the following request body: "password": "password-sabin" } ``` + After executing the request you should get a JWT in the response. ![`POST /auth/login` endpoint](/nestjs-prisma-authentication-7D056s1s0k3l/imgs/auth-login-endpoint.png) In the next section, you will use this token to authenticate users. - ### Implement JWT authentication strategy In Passport, a [strategy](https://www.passportjs.org/concepts/authentication/strategies/) is responsible for authenticating requests, which it accomplishes by implementing an authentication mechanism. In this section, you will implement a JWT authentication strategy that will be used to authenticate users. @@ -367,20 +359,22 @@ You will not be using the `passport` package directly, but rather interact with 1. You will pass JWT strategy specific options and configuration to the `super()` method in the constructor. 2. A `validate()` callback method that will interact with your database to fetch a user based on the JWT payload. If a user is found, the `validate()` method is expected to return the user object. -First create a new file called `jwt.strategy.ts` inside the `src/auth/strategy` directory: +First create a new file called `jwt.strategy.ts` inside the `src/auth` directory: ```shell touch src/auth/jwt.strategy.ts ``` + Now implement the `JwtStrategy` class: ```ts -//src/auth/jwt.strategy.ts +// src/auth/jwt.strategy.ts + import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { jwtSecret } from './auth.module'; -import { UsersService } from 'src/users/users.service'; +import { UsersService } from '../users/users.service'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { @@ -402,29 +396,28 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { } } ``` -You have created a `JwtStrategy` class that extends the `PassportStrategy` class. The `PassportStrategy` class takes two arguments: a strategy implementation and the name of the strategy. Here you are using a predefined strategy from the `passport-jwt` library. - -You are passing some options to the `super()` method in the constructor. The `jwtFromRequest` option expects a method that can be used to extract the JWT from the request. In this case, you will use the standard approach of supplying a bearer token in the Authorization header of our API requests. The `secretOrKey` option tells the strategy what secret to use to verify the JWT. There are many more options, which you can read about in the [`passport-jwt` repository.](https://github.com/mikenicholson/passport-jwt#configure-strategy). -For the `passport-jwt`, Passport first verifies the JWT's signature and decodes the JSON. The decoded JSON is then passed to the `validate()` method. Based on the way JWT signing works, you're guaranteed receiving a valid token that was previously signed and issued by your app. The `validate()` method is expected to return a user object. If the user is not found, the `validate()` method throws an error. +You have created a `JwtStrategy` class that extends the `PassportStrategy` class. The `PassportStrategy` class takes two arguments: a strategy implementation and the name of the strategy. Here you are using a predefined strategy from the `passport-jwt` library. -> **Note**: Passport can be quite confusing. It's helpful to think of Passport as a mini framework in itself that abstracts the authentication process into a few steps that can be customized with strategies and configuration options. I reccomend reading the [NestJS Passport recipe](https://docs.nestjs.com/recipes/passport) to learn more about how to use Passport with NestJS. +You are passing some options to the `super()` method in the constructor. The `jwtFromRequest` option expects a method that can be used to extract the JWT from the request. In this case, you will use the standard approach of supplying a bearer token in the Authorization header of your API requests. The `secretOrKey` option tells the strategy what secret to use to verify the JWT. There are many more options, which you can read about in the [`passport-jwt` repository](https://github.com/mikenicholson/passport-jwt#configure-strategy). +For the `passport-jwt` strategy, Passport first verifies the JWT's signature and decodes the JSON. The decoded JSON is then passed to the `validate()` method. Based on the way JWT signing works, you're guaranteed to receive a valid token that was previously signed and issued by your app. The `validate()` method is expected to return a user object. If the user is not found, the `validate()` method throws an error. +> **Note**: Passport can be quite confusing. It's helpful to think of Passport as a mini framework in itself that abstracts the authentication process into a few steps that can be customized with strategies and configuration options. I recommend reading the [NestJS Passport recipe](https://docs.nestjs.com/recipes/passport) to learn more about how to use Passport with NestJS. Add the new `JwtStrategy` as a provider in the `AuthModule`: - ```ts -//src/auth/auth.module.ts +// src/auth/auth.module.ts + import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; import { AuthController } from './auth.controller'; import { PassportModule } from '@nestjs/passport'; import { JwtModule } from '@nestjs/jwt'; -import { PrismaModule } from 'src/prisma/prisma.module'; -+import { UsersModule } from 'src/users/users.module'; -+import { JwtStrategy } from './jwt.strategy'; +import { PrismaModule } from '../prisma/prisma.module'; +import { UsersModule } from '../users/users.module'; +import { JwtStrategy } from './jwt.strategy'; export const jwtSecret = 'zjP9h6ZI5LoSKCRj'; @@ -434,34 +427,37 @@ export const jwtSecret = 'zjP9h6ZI5LoSKCRj'; PassportModule, JwtModule.register({ secret: jwtSecret, - signOptions: { expiresIn: '5m' }, // e.g. 7d, 24h + signOptions: { expiresIn: '5m' }, // e.g. 30s, 7d, 24h }), -+ UsersModule, + UsersModule, ], controllers: [AuthController], -+ providers: [AuthService, JwtStrategy], + providers: [AuthService, JwtStrategy], }) export class AuthModule {} ``` + Now the `JwtStrategy` can be used by other modules. You have also added the `UsersModule` in the `imports`, because the `UsersService` is being used in the `JwtStrategy` class. To make `UsersService` accessible in the `JwtStrategy` class, you also need to add it in the `exports` of the `UsersModule`: ```ts // src/users/users.module.ts + import { Module } from '@nestjs/common'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; -import { PrismaModule } from 'src/prisma/prisma.module'; +import { PrismaModule } from '../prisma/prisma.module'; @Module({ controllers: [UsersController], providers: [UsersService], imports: [PrismaModule], -+ exports: [UsersService], + exports: [UsersService], }) export class UsersModule {} ``` + ### Implement JWT auth guard [Guards](https://docs.nestjs.com/guards) are a NestJS construct that determines whether a request should be allowed to proceed or not. In this section, you will implement a custom `JwtAuthGuard` that will be used to protect routes that require authentication. @@ -471,24 +467,26 @@ Create a new file called `jwt-auth.guard.ts` inside the `src/auth` directory: ```shell touch src/auth/jwt-auth.guard.ts ``` + Now implement the `JwtAuthGuard` class: ```ts -//src/auth/jwt-auth.guard.ts +// src/auth/jwt-auth.guard.ts + import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') {} ``` -The `AuthGuard` class expects the name of a strategy. In this case, you are using the `JwtStrategy` that you implemented in the previous section, which is named `jwt`. +The `AuthGuard` class expects the name of a strategy. In this case, you are using the `JwtStrategy` that you implemented in the previous section, which is named `jwt`. You can now use this guard as a decorator to protect your endpoints. Add the `JwtAuthGuard` to routes in the `UsersController`: - ```ts // src/users/users.controller.ts + import { Controller, Get, @@ -498,14 +496,15 @@ import { Param, Delete, ParseIntPipe, -+ UseGuards, + NotFoundException, + UseGuards, } from '@nestjs/common'; import { UsersService } from './users.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { UserEntity } from './entities/user.entity'; -+import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; @Controller('users') @ApiTags('users') @@ -519,7 +518,7 @@ export class UsersController { } @Get() -+ @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard) @ApiOkResponse({ type: UserEntity, isArray: true }) async findAll() { const users = await this.usersService.findAll(); @@ -527,15 +526,19 @@ export class UsersController { } @Get(':id') -+ @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard) @ApiOkResponse({ type: UserEntity }) async findOne(@Param('id', ParseIntPipe) id: number) { - return new UserEntity(await this.usersService.findOne(id)); + const user = await this.usersService.findOne(id); + if (!user) { + throw new NotFoundException(`User with ${id} does not exist.`); + } + return new UserEntity(user); } @Patch(':id') -+ @UseGuards(JwtAuthGuard) - @ApiCreatedResponse({ type: UserEntity }) + @UseGuards(JwtAuthGuard) + @ApiOkResponse({ type: UserEntity }) async update( @Param('id', ParseIntPipe) id: number, @Body() updateUserDto: UpdateUserDto, @@ -544,103 +547,69 @@ export class UsersController { } @Delete(':id') -+ @UseGuards(JwtAuthGuard) + @UseGuards(JwtAuthGuard) @ApiOkResponse({ type: UserEntity }) async remove(@Param('id', ParseIntPipe) id: number) { return new UserEntity(await this.usersService.remove(id)); } } ``` -If you try to query any of these endpoints without authentication it will no longer work. -![`GET /users endpoint gives 401 response](/nestjs-prisma-authentication-7D056s1s0k3l/imgs/401-GET-users.png) +If you try to query any of these endpoints without authentication it will no longer work. Instead, you get an HTTP 401 response: + +```json +{ + "message": "Unauthorized", + "statusCode": 401 +} +``` + +![`GET /users` endpoint gives 401 response](/nestjs-prisma-authentication-7D056s1s0k3l/imgs/401-GET-users.png) ### Integrate authentication in Swagger -Currently there's no indication on Swagger that these endpoints are auth protected. You can add a `@ApiBearerAuth()` decorator to the controller to indicate that authentication is required: +Currently there's no indication on Swagger that these endpoints are auth protected. You can add an `@ApiBearerAuth()` decorator to each guarded route handler to indicate that authentication is required: ```ts // src/users/users.controller.ts import { - Controller, - Get, - Post, - Body, - Patch, - Param, - Delete, - ParseIntPipe, - UseGuards, -} from '@nestjs/common'; -import { UsersService } from './users.service'; -import { CreateUserDto } from './dto/create-user.dto'; -import { UpdateUserDto } from './dto/update-user.dto'; -+import { ApiBearerAuth, ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger'; -import { UserEntity } from './entities/user.entity'; -import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; + ApiBearerAuth, + ApiCreatedResponse, + ApiOkResponse, + ApiTags, +} from '@nestjs/swagger'; -@Controller('users') -@ApiTags('users') export class UsersController { - constructor(private readonly usersService: UsersService) {} - - @Post() - @ApiCreatedResponse({ type: UserEntity }) - async create(@Body() createUserDto: CreateUserDto) { - return new UserEntity(await this.usersService.create(createUserDto)); - } + // ... @Get() @UseGuards(JwtAuthGuard) -+ @ApiBearerAuth() + @ApiBearerAuth() @ApiOkResponse({ type: UserEntity, isArray: true }) async findAll() { const users = await this.usersService.findAll(); return users.map((user) => new UserEntity(user)); } - @Get(':id') - @UseGuards(JwtAuthGuard) -+ @ApiBearerAuth() - @ApiOkResponse({ type: UserEntity }) - async findOne(@Param('id', ParseIntPipe) id: number) { - return new UserEntity(await this.usersService.findOne(id)); - } - - @Patch(':id') - @UseGuards(JwtAuthGuard) -+ @ApiBearerAuth() - @ApiCreatedResponse({ type: UserEntity }) - async update( - @Param('id', ParseIntPipe) id: number, - @Body() updateUserDto: UpdateUserDto, - ) { - return new UserEntity(await this.usersService.update(id, updateUserDto)); - } - - @Delete(':id') - @UseGuards(JwtAuthGuard) -+ @ApiBearerAuth() - @ApiOkResponse({ type: UserEntity }) - async remove(@Param('id', ParseIntPipe) id: number) { - return new UserEntity(await this.usersService.remove(id)); - } + // repeat for findOne, update and remove } ``` -Now, auth protected endpoints should have a lock icon in Swagger 🔓 -![`Auth protected endpoints in Swagger`](/nestjs-prisma-authentication-7D056s1s0k3l/imgs/locked-endpoints.png) +Add the decorator to the `findOne`, `update` and `remove` handlers in the same way. Now, auth protected endpoints should have a lock icon in Swagger 🔓 + +![Auth protected endpoints in Swagger](/nestjs-prisma-authentication-7D056s1s0k3l/imgs/locked-endpoints.png) It's currently not possible to "authenticate" yourself directly in Swagger so you can test these endpoints. To do this, you can add the `.addBearerAuth()` method call to the `SwaggerModule` setup in `main.ts`: ```ts // src/main.ts -import { NestFactory, Reflector } from '@nestjs/core'; +import { HttpAdapterHost, NestFactory, Reflector } from '@nestjs/core'; import { AppModule } from './app.module'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common'; +import { PrismaClientExceptionFilter } from './prisma-client-exception/prisma-client-exception.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -652,18 +621,23 @@ async function bootstrap() { .setTitle('Median') .setDescription('The Median API description') .setVersion('0.1') -+ .addBearerAuth() + .addBearerAuth() .build(); + const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); - await app.listen(3000); + const { httpAdapter } = app.get(HttpAdapterHost); + app.useGlobalFilters(new PrismaClientExceptionFilter(httpAdapter)); + + await app.listen(process.env.PORT ?? 3000); } bootstrap(); ``` + You can now add a token by clicking on the **Authorize** button in Swagger. Swagger will add the token to your requests so you can query the protected endpoints. -> **Note**: You can generate a token by sending a `POST` request to `/auth/login` endpoint with a valid `email` and `password`. +> **Note**: You can generate a token by sending a `POST` request to the `/auth/login` endpoint with a valid `email` and `password`. Try it out yourself. @@ -680,29 +654,31 @@ You can use the `bcrypt` cryptography library to hash passwords. Install it with npm install bcrypt npm install --save-dev @types/bcrypt ``` + First, you will update the `create` and `update` methods in the `UsersService` to hash the password before storing it in the database: ```ts // src/users/users.service.ts + import { Injectable } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; -import { PrismaService } from 'src/prisma/prisma.service'; -+import * as bcrypt from 'bcrypt'; +import { PrismaService } from '../prisma/prisma.service'; +import * as bcrypt from 'bcrypt'; -+export const roundsOfHashing = 10; +export const roundsOfHashing = 10; @Injectable() export class UsersService { constructor(private prisma: PrismaService) {} -+ async create(createUserDto: CreateUserDto) { -+ const hashedPassword = await bcrypt.hash( -+ createUserDto.password, -+ roundsOfHashing, -+ ); + async create(createUserDto: CreateUserDto) { + const hashedPassword = await bcrypt.hash( + createUserDto.password, + roundsOfHashing, + ); -+ createUserDto.password = hashedPassword; + createUserDto.password = hashedPassword; return this.prisma.user.create({ data: createUserDto, @@ -717,13 +693,13 @@ export class UsersService { return this.prisma.user.findUnique({ where: { id } }); } -+ async update(id: number, updateUserDto: UpdateUserDto) { -+ if (updateUserDto.password) { -+ updateUserDto.password = await bcrypt.hash( -+ updateUserDto.password, -+ roundsOfHashing, -+ ); -+ } + async update(id: number, updateUserDto: UpdateUserDto) { + if (updateUserDto.password) { + updateUserDto.password = await bcrypt.hash( + updateUserDto.password, + roundsOfHashing, + ); + } return this.prisma.user.update({ where: { id }, @@ -736,100 +712,110 @@ export class UsersService { } } ``` -The `bcrypt.hash` function accepts two arguments: the input string to the hash function and the number of rounds of hashing (also known as cost factor). Increasing the rounds of hashing increases the time it takes to calculate the hash. There is a trade off here between security and performance. With more rounds of hashing, it takes more time to calculate the hash, which helps prevent brute force attacks. However, more rounds of hashing also mean more time to calculate the hash when a user logs in. [This stack overflow answer](https://security.stackexchange.com/a/17207) has a good discussion on this topic. -`bcrypt` also automatically uses another technique called [salting](https://en.wikipedia.org/wiki/Salt_(cryptography)) to make it harder to brute force the hash. Salting is a technique where a random string is added to the input string before hashing. This way, attackers cannot use a table of precomputed hashes to crack the password, as each password has a different salt value. +The `bcrypt.hash` function accepts two arguments: the input string to the hash function and the number of rounds of hashing (also known as cost factor). Increasing the rounds of hashing increases the time it takes to calculate the hash. There is a trade off here between security and performance. With more rounds of hashing, it takes more time to calculate the hash, which helps prevent brute force attacks. However, more rounds of hashing also mean more time to calculate the hash when a user logs in. [This Stack Exchange answer](https://security.stackexchange.com/a/17207) has a good discussion on this topic. + +`bcrypt` also automatically uses another technique called [salting]() to make it harder to brute force the hash. Salting is a technique where a random string is added to the input string before hashing. This way, attackers cannot use a table of precomputed hashes to crack the password, as each password has a different salt value. You also need to update your database seed script to hash the passwords before inserting them into the database: ```ts // prisma/seed.ts -import { PrismaClient } from '@prisma/client'; -+import * as bcrypt from 'bcrypt'; -// initialize the Prisma Client -const prisma = new PrismaClient(); +import 'dotenv/config'; +import { PrismaPg } from '@prisma/adapter-pg'; +import { PrismaClient } from '../generated/prisma/client'; +import * as bcrypt from 'bcrypt'; + +// initialize Prisma Client with the PostgreSQL driver adapter +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); +const prisma = new PrismaClient({ adapter }); -+const roundsOfHashing = 10; +const roundsOfHashing = 10; async function main() { // create two dummy users -+ const passwordSabin = await bcrypt.hash('password-sabin', roundsOfHashing); -+ const passwordAlex = await bcrypt.hash('password-alex', roundsOfHashing); + const passwordSabin = await bcrypt.hash('password-sabin', roundsOfHashing); + const passwordAlex = await bcrypt.hash('password-alex', roundsOfHashing); const user1 = await prisma.user.upsert({ where: { email: 'sabin@adams.com' }, -+ update: { -+ password: passwordSabin, -+ }, + update: { + password: passwordSabin, + }, create: { email: 'sabin@adams.com', name: 'Sabin Adams', -+ password: passwordSabin, + password: passwordSabin, }, }); const user2 = await prisma.user.upsert({ where: { email: 'alex@ruheni.com' }, -+ update: { -+ password: passwordAlex, -+ }, + update: { + password: passwordAlex, + }, create: { email: 'alex@ruheni.com', name: 'Alex Ruheni', -+ password: passwordAlex, + password: passwordAlex, }, }); - // create three dummy posts + // create three dummy articles // ... } // execute the main function // ... ``` -Run the seed script with `npx prisma db seed` and you should see that the passwords stored in the database are now hashed. + +Run the seed script with `npx prisma db seed` and you should see that the passwords stored in the database are now hashed: + ``` -... -Running seed command `ts-node prisma/seed.ts` ... +Running seed command `tsx prisma/seed.ts` ... { user1: { id: 1, name: 'Sabin Adams', email: 'sabin@adams.com', - password: '$2b$10$XKQvtyb2Y.jciqhecnO4QONdVVcaghDgLosDPeI0e90POYSPd1Dlu', - createdAt: 2023-03-20T22:05:56.758Z, - updatedAt: 2023-04-02T22:58:05.792Z + password: '$2b$10$u0f/Onnvf90ujN4Dfna2le8209CeLYgfinGTc6d8cfzN90nDRa.R6', + ... }, user2: { - id: 2, - name: 'Alex Ruheni', - email: 'alex@ruheni.com', - password: '$2b$10$0tEfezrEd1a2g51lJBX6t.Tn.RLppKTv14mucUSCv40zs5qQyBaw6', - createdAt: 2023-03-20T22:05:56.772Z, - updatedAt: 2023-04-02T22:58:05.808Z -}, -... + id: 2, + name: 'Alex Ruheni', + email: 'alex@ruheni.com', + password: '$2b$10$Tp0gQUJYmFHaahp7OUd67eh.0MgZssgrBR9kqXR.cC6Dli1TMnKlC', + ... + }, + ... +} ``` + The value of the `password` field will be different for you since a different salt value is used each time. The important thing is that the value is now a hashed string. -Now, if you try to use the `login` with the correct password, you will face a `HTTP 401` error. This is because the `login` method tries to compare the plaintext password from the user request with the hashed password in the database. Update the `login` method to use hashed passwords: +Now, if you try to use the `login` endpoint with the correct password, you will face an HTTP 401 error. This is because the `login` method tries to compare the plaintext password from the user request with the hashed password in the database. Update the `login` method to use hashed passwords: ```ts -//src/auth/auth.service.ts -import { AuthEntity } from './entity/auth.entity'; -import { PrismaService } from './../prisma/prisma.service'; +// src/auth/auth.service.ts + import { Injectable, NotFoundException, UnauthorizedException, } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; import { JwtService } from '@nestjs/jwt'; -+import * as bcrypt from 'bcrypt'; +import { AuthEntity } from './entity/auth.entity'; +import * as bcrypt from 'bcrypt'; @Injectable() export class AuthService { - constructor(private prisma: PrismaService, private jwtService: JwtService) {} + constructor( + private prisma: PrismaService, + private jwtService: JwtService, + ) {} async login(email: string, password: string): Promise { const user = await this.prisma.user.findUnique({ where: { email } }); @@ -838,7 +824,7 @@ export class AuthService { throw new NotFoundException(`No user found for email: ${email}`); } -+ const isPasswordValid = await bcrypt.compare(password, user.password); + const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { throw new UnauthorizedException('Invalid password'); @@ -850,18 +836,14 @@ export class AuthService { } } ``` -You can now login with the correct password and get a JWT in the response. +You can now login with the correct password and get a JWT in the response. ## Summary and final remarks - In this chapter, you learned how to implement JWT authentication in your NestJS REST API. You also learned about salting passwords and integrating authentication with Swagger. -To keep the series moving, review the [Prisma getting started guide](https://www.prisma.io/docs/getting-started), explore [Prisma Migrate](https://www.prisma.io/docs/orm/prisma-migrate/getting-started) for schema changes, or use [Prisma Postgres](https://www.prisma.io/postgres) as the database for your next NestJS API. - -You can find the finished code for this tutorial in the [`end-authentication`](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma/tree/end-authentication) branch of the [GitHub repository](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma). Please feel free to raise an issue in the repository or submit a PR if you notice a problem. You can also reach out to me directly on [Twitter](https://twitter.com/tasinishmam). - - +This wraps up the series. Across five chapters, you built a REST API with NestJS 11 and Prisma ORM 7, added input validation, error handling, relational data and authentication; every piece verified on the current stack. From here, good next steps are [The Ultimate Guide to Testing with Prisma](https://www.prisma.io/blog/series/testing-with-prisma) for test coverage, the [Prisma Migrate docs](https://www.prisma.io/docs/orm/prisma-migrate/getting-started) for evolving your schema, and [Prisma Postgres](https://www.prisma.io/postgres) as the database for your production deployment. +Looking ahead: [Prisma Next](https://www.prisma.io/docs/orm) is a TypeScript-native rewrite of Prisma ORM, built for AI coding agents and currently in early access. It becomes Prisma 8 at general availability; until then, Prisma 7 stays the production choice. To try it, run `npm create prisma@next` or read the [early access docs](https://pris.ly/pn-ea). diff --git a/apps/blog/content/blog/nestjs-prisma-error-handling-7D056s1kOop2/index.mdx b/apps/blog/content/blog/nestjs-prisma-error-handling-7D056s1kOop2/index.mdx index 77e6006b6b..a981da6413 100644 --- a/apps/blog/content/blog/nestjs-prisma-error-handling-7D056s1kOop2/index.mdx +++ b/apps/blog/content/blog/nestjs-prisma-error-handling-7D056s1kOop2/index.mdx @@ -2,10 +2,11 @@ title: "Building a REST API with NestJS and Prisma: Error Handling" slug: "nestjs-prisma-error-handling-7D056s1kOop2" date: "2022-12-14" +updatedAt: "2026-07-08" authors: - "Tasin Ishmam" -metaTitle: "Building a REST API with NestJS and Prisma: Error Handling" -metaDescription: "In this tutorial, you will implement error handling in a NestJS application. You will learn two ways to handle errors: directly in your application code and by creating an exception filter." +metaTitle: "Error Handling in a REST API with NestJS and Prisma 7" +metaDescription: "Learn how to handle Prisma errors in a NestJS REST API: throw exceptions in controllers and build an exception filter for Prisma error codes like P2002. Updated for Prisma ORM 7 and NestJS 11." metaImagePath: "/nestjs-prisma-error-handling-7D056s1kOop2/imgs/meta-61137d012c97c0dc9c0a72557fe3a277a11700a4-1272x716.png" heroImagePath: "/nestjs-prisma-error-handling-7D056s1kOop2/imgs/hero-c86e8991069ead629ccc386f69880911bbb014c8-844x474.svg" heroImageAlt: "Building a REST API with NestJS and Prisma: Error Handling" @@ -19,14 +20,15 @@ tags: 6 min read -Welcome to the third tutorial in this series about building a REST API with NestJS, Prisma and PostgreSQL! -In this tutorial, you will learn how to perform error handling in a NestJS application. +Welcome to the third tutorial in this series about building a REST API with NestJS and Prisma! In this tutorial, you will learn how to handle errors in a NestJS application, using two strategies: throwing exceptions directly in your route handlers, and building an exception filter that turns Prisma error codes into meaningful HTTP responses. + +> **Updated (July 2026):** This tutorial has been fully revised for **Prisma ORM 7** and **NestJS 11**, and continues from the updated [first](/nestjs-prisma-rest-api-7D056s1BmOL0) and [second](/nestjs-prisma-validation-7D056s1kOla1) parts of the series. In Prisma 7 the `Prisma` namespace (including error classes like `PrismaClientKnownRequestError`) is imported from your generated Client instead of `@prisma/client`. Every command and code block below was run end-to-end against `prisma@7.8`, `@prisma/client@7.8` and `@nestjs/core@11`. ## Table Of Contents - [Introduction](#introduction) - [Development environment](#development-environment) - - [Clone the repository](#clone-the-repository) + - [Set up the project](#set-up-the-project) - [Project structure and files](#project-structure-and-files) - [Detect and throw exceptions directly](#detect-and-throw-exceptions-directly) - [Handle exceptions by using exception filters](#handle-exceptions-by-using-exception-filters) @@ -40,100 +42,83 @@ In this tutorial, you will learn how to perform error handling in a NestJS appli ## Introduction -In the [first chapter](/nestjs-prisma-rest-api-7D056s1BmOL0) of this series, you created a new NestJS project and integrated it with Prisma, PostgreSQL and Swagger. Then, you built a rudimentary REST API for the backend of a blog application. In the [second chapter](/nestjs-prisma-validation-7D056s1kOla1) you learnt how to do input validation and transformation. +In the [first chapter](/nestjs-prisma-rest-api-7D056s1BmOL0) of this series, you created a new NestJS project and integrated it with Prisma ORM, PostgreSQL and Swagger. Then, you built a rudimentary REST API for the backend of a blog application. In the [second chapter](/nestjs-prisma-validation-7D056s1kOla1) you learned how to do input validation and transformation. In this chapter you will learn how to handle errors in NestJS. You will look at two different strategies: 1. First, you will learn how to detect and throw errors directly in your application code inside the controllers of your API. 2. Next, you will learn how to use an [exception filter](https://docs.nestjs.com/exception-filters) to process unhandled exceptions throughout your application. -In this tutorial, you will be using the REST API built in the [first chapter](/nestjs-prisma-rest-api-7D056s1BmOL0). You do not need to complete the [second chapter](/nestjs-prisma-validation-7D056s1kOla1) to follow this tutorial. - +This tutorial continues from the end of the [second chapter](/nestjs-prisma-validation-7D056s1kOla1). Only the first chapter is strictly required; if you skipped the second one, your route handlers will still accept the `id` parameter as a `string` and cast it with `+id`, but the error handling techniques below work the same way. ### Development environment To follow along with this tutorial, you will be expected to: -- ... have [Node.js](https://nodejs.org) installed. -- ... have [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/install/#compose-installation-scenarios) installed. If you are using Linux, please make sure your Docker version is 20.10.0 or higher. You can check your Docker version by running `docker version` in the terminal. -- ... _optionally_ have the [Prisma VS Code Extension](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma) installed. The Prisma VS Code extension adds some really nice IntelliSense and syntax highlighting for Prisma. -- ... _optionally_ have access to a Unix shell (like the terminal/shell in Linux and macOS) to run the commands provided in this series. +- ... have [Node.js](https://nodejs.org) (v18 or higher) installed. +- ... have a PostgreSQL database. The easiest option is [Prisma Postgres](https://www.prisma.io/postgres); you can also run Postgres locally with [Docker](https://www.docker.com/) or a native install. +- ... have the [Prisma VSCode Extension](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma) installed. _(optional)_ +- ... have access to a Unix shell (like the terminal/shell in Linux and macOS) to run the commands provided in this series. _(optional)_ + +> **Note**: If you don't have a Unix shell (for example, you are on a Windows machine), you can still follow along, but the shell commands may need to be modified for your machine. -If you don't have a Unix shell (for example, you are on a Windows machine), you can still follow along, but the shell commands may need to be modified for your machine. +### Set up the project -### Clone the repository +The starting point for this tutorial is the ending of [part two](/nestjs-prisma-validation-7D056s1kOla1) of this series: the Median REST API built with NestJS 11 and Prisma ORM 7, including input validation. If you're jumping in here, work through the earlier parts first; each one builds directly on the previous. -The starting point for this tutorial is the ending of [part one](/nestjs-prisma-rest-api-7D056s1BmOL0) of this series. It contains a rudimentary REST API built with NestJS. +Before continuing, make sure the project runs: -The starting point for this tutorial is available in the [`end-rest-api-part-1`](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma/tree/end-rest-api-part-1) branch of the [GitHub repository](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma). To get started, clone the repository and checkout the `end-rest-api-part-1` branch: +1. Start the development server: ```shell -git clone -b end-rest-api-part-1 git@github.com:prisma/blog-backend-rest-api-nestjs-prisma.git +npm run start:dev ``` -Now, perform the following actions to get started: - -1. Navigate to the cloned directory: - ```shell cd blog-backend-rest-api-nestjs-prisma - ``` -2. Install dependencies: - ```shell npm install - ``` -3. Start the PostgreSQL database with Docker: - ```shell docker-compose up -d - ``` -4. Apply database migrations: - ```shell npx prisma migrate dev - ``` -5. Start the project: - ```shell npm run start:dev - ``` - -> **Note**: Step 4 will also generate Prisma Client and seed the database. - -Now, you should be able to access the API documentation at [`http://localhost:3000/api/`](http://localhost:3000/api/). + +2. Confirm the API documentation is available at [`http://localhost:3000/api/`](http://localhost:3000/api/). + +> **Note**: The original edition of this series linked to a companion GitHub repository. That repository still targets the older Prisma 4 and NestJS 8 stack, so for the updated series you should continue from your own project. ### Project structure and files -The repository you cloned should have the following structure: +Your project should have the following structure: + ``` median ├── node_modules + ├── generated + │ └── prisma ├── prisma │ ├── migrations │ ├── schema.prisma │ └── seed.ts ├── src - │ ├── app.controller.spec.ts - │ ├── app.controller.ts │ ├── app.module.ts - │ ├── app.service.ts │ ├── main.ts │ ├── articles │ └── prisma ├── test - │   ├── app.e2e-spec.ts - │   └── jest-e2e.json + │ ├── app.e2e-spec.ts + │ └── jest-e2e.json ├── README.md ├── .env - ├── docker-compose.yml + ├── eslint.config.mjs ├── nest-cli.json ├── package-lock.json ├── package.json + ├── prisma.config.ts ├── tsconfig.build.json └── tsconfig.json ``` + The notable files and directories in this repository are: - The `src` directory contains the source code for the application. There are three modules: - The `app` module is situated in the root of the `src` directory and is the entry point of the application. It is responsible for starting the web server. - - The `prisma` module contains Prisma Client, your interface to the database. + - The `prisma` module contains `PrismaService`, your interface to the database. - The `articles` module defines the endpoints for the `/articles` route and accompanying business logic. -- The `prisma` module has the following: - - The `schema.prisma` file defines the database schema. - - The `migrations` directory contains the database migration history. - - The `seed.ts` file contains a script to seed your development database with dummy data. -- The `docker-compose.yml` file defines the Docker image for your PostgreSQL database. -- The `.env` file contains the database connection string for your PostgreSQL database. +- The `prisma` directory contains the `schema.prisma` file (database schema), the `migrations` directory (migration history) and the `seed.ts` script. +- The `generated/prisma` directory contains the generated Prisma Client, which in Prisma 7 lives in your project instead of `node_modules`. +- The `prisma.config.ts` file is Prisma's central configuration file, and `.env` contains the `DATABASE_URL` connection string. > **Note**: For more information about these components, go through [part one](/nestjs-prisma-rest-api-7D056s1BmOL0) of this tutorial series. @@ -142,16 +127,13 @@ The notable files and directories in this repository are: This section will teach you how to throw exceptions directly in your application code. You will address an issue in the `GET /articles/:id` endpoint. Currently, if you provide this endpoint with an `id` value that does not exist, it will return nothing with an HTTP `200` status instead of an error. -For example, try making a `GET /articles/234235` request: - - -{/* ![Requesting an article that does not exist returns HTTP 200](/nestjs-error-handling/article-not-exist-200.png) */} +For example, try making a `GET /articles/234235` request. You get a `200` response with an empty body, which is misleading for API consumers. To fix this, you have to change the `findOne` method in `articles.controller.ts`. If the article does not exist, you will throw a `NotFoundException`, a built-in exception provided by NestJS. Update the `findOne` method in `articles.controller.ts`: -```typescript +```ts // src/articles/articles.controller.ts import { @@ -162,25 +144,32 @@ import { Patch, Param, Delete, -+ NotFoundException, + ParseIntPipe, + NotFoundException, } from '@nestjs/common'; @Get(':id') @ApiOkResponse({ type: ArticleEntity }) -- findOne(@Param('id') id: string) { -- return this.articlesService.findOne(+id); -+ async findOne(@Param('id') id: string) { -+ const article = await this.articlesService.findOne(+id); -+ if (!article) { -+ throw new NotFoundException(`Article with ${id} does not exist.`); -+ } -+ return article; + async findOne(@Param('id', ParseIntPipe) id: number) { + const article = await this.articlesService.findOne(id); + if (!article) { + throw new NotFoundException(`Article with ${id} does not exist.`); + } + return article; } ``` -If you make that same request again, you should get a user friendly error message: -![Requesting an article that does not exist returns HTTP 404](/nestjs-prisma-error-handling-7D056s1kOop2/imgs/article-not-exist-404.png) +The route handler is now `async` and awaits the result of the service call, so it can inspect the result before returning it. If you make that same request again, you should get a user friendly error message: +```json +{ + "message": "Article with 234235 does not exist.", + "error": "Not Found", + "statusCode": 404 +} +``` + +![Requesting an article that does not exist returns HTTP 404](/nestjs-prisma-error-handling-7D056s1kOop2/imgs/article-not-exist-404.png) ## Handle exceptions by using exception filters @@ -199,16 +188,17 @@ To solve these issues, NestJS has an [exception layer](https://docs.nestjs.com/e ### NestJS global exception filter -NestJS has a global exception filter, which catches all unhandled exceptions. To understand the global exception filter, let's look at an example. Send _two_ requests to the `POST /articles` endpoints with the following body: +NestJS has a global exception filter, which catches all unhandled exceptions. To understand the global exception filter, let's look at an example. Send _two_ requests to the `POST /articles` endpoint with the following body: ```json { - "title": "Let’s build a REST API with NestJS and Prisma.", + "title": "Let's build a REST API with NestJS and Prisma.", "description": "NestJS Series announcement.", "body": "NestJS is one of the hottest Node.js frameworks around. In this series, you will learn how to build a backend REST API with NestJS, Prisma, PostgreSQL and Swagger.", "published": true } ``` + The first request will succeed, but the second request will fail because you already created an article with the same `title` field. You will get the following error: ```json @@ -217,27 +207,26 @@ The first request will succeed, but the second request will fail because you alr "message": "Internal server error" } ``` + If you take a look at the terminal window running your NestJS server, you should see the following error: + ``` -[Nest] 6803 - 12/06/2022, 3:25:40 PM ERROR [ExceptionsHandler] +[Nest] 9112 - 07/08/2026, 10:33:41 AM ERROR [ExceptionsHandler] PrismaClientKnownRequestError: Invalid `this.prisma.article.create()` invocation in -/Users/tasinishmam/my-code/median/src/articles/articles.service.ts:11:32 +~/median/src/articles/articles.service.ts:11:32 8 constructor(private prisma: PrismaService) {} 9 10 create(createArticleDto: CreateArticleDto) { → 11 return this.prisma.article.create( - Unique constraint failed on the fields: (`title`) -Error: -Invalid `this.prisma.article.create()` invocation in -/Users/tasinishmam/my-code/median/src/articles/articles.service.ts:11:32 -8 constructor(private prisma: PrismaService) {} - 9 - 10 create(createArticleDto: CreateArticleDto) { -→ 11 return this.prisma.article.create( - Unique constraint failed on the fields: (`title`) +Unique constraint failed on the fields: (`title`) + at ... { + code: 'P2002', + ... +} ``` -From the logs you can see that Prisma Client throws an unique constraint validation error because of the `title` field, which is marked as `@unique` in the Prisma schema. The exception is of type `PrismaClientKnownRequestError` and is exported at the Prisma namespace level. + +From the logs you can see that Prisma Client throws a unique constraint validation error because of the `title` field, which is marked as `@unique` in the Prisma schema. The exception is of type `PrismaClientKnownRequestError` and is exported at the `Prisma` namespace level of your generated Client. Since the `PrismaClientKnownRequestError` is not being handled directly by your application, it is automatically processed by the built-in global exception filter. This filter generates the HTTP `500` "Internal Server Error" response. @@ -250,10 +239,11 @@ Start by generating a filter class by using the Nest CLI: ```shell npx nest generate filter prisma-client-exception ``` -This will create a new file `src/prisma-client-exception.filter.ts` with the following content: -```typescript -// src/prisma-client-exception.filter.ts +This will create a new file `src/prisma-client-exception/prisma-client-exception.filter.ts` with the following content: + +```ts +// src/prisma-client-exception/prisma-client-exception.filter.ts import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; @@ -262,73 +252,75 @@ export class PrismaClientExceptionFilter implements ExceptionFilter { catch(exception: T, host: ArgumentsHost) {} } ``` -> **Note**: There is a second file created called `src/prisma-client-exception.filter.spec.ts` for creating tests. You can ignore this file for now. + +> **Note**: There is a second file created called `src/prisma-client-exception/prisma-client-exception.filter.spec.ts` for creating tests. You can ignore this file for now. You will get an error from `eslint` since the `catch` method is empty. Update the `catch` method implementation in `PrismaClientExceptionFilter` as follows: -```typescript -// src/prisma-client-exception.filter.ts +```ts +// src/prisma-client-exception/prisma-client-exception.filter.ts -+import { ArgumentsHost, Catch } from '@nestjs/common'; -+import { BaseExceptionFilter } from '@nestjs/core'; -+import { Prisma } from '@prisma/client'; +import { ArgumentsHost, Catch } from '@nestjs/common'; +import { BaseExceptionFilter } from '@nestjs/core'; +import { Prisma } from '../../generated/prisma/client'; -+@Catch(Prisma.PrismaClientKnownRequestError) // 1 -+export class PrismaClientExceptionFilter extends BaseExceptionFilter { // 2 -+ catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) { -+ console.error(exception.message); // 3 +@Catch(Prisma.PrismaClientKnownRequestError) // 1 +export class PrismaClientExceptionFilter extends BaseExceptionFilter { // 2 + catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) { + console.error(exception.message); // 3 // default 500 error code -+ super.catch(exception, host); + super.catch(exception, host); } } ``` + Here you have made the following changes: -1. To ensure that this filter catches exceptions of type `PrismaClientKnownRequestError`, you added it to the `@Catch` decorator. +1. To ensure that this filter catches exceptions of type `PrismaClientKnownRequestError`, you added it to the `@Catch` decorator. Note the import: in Prisma 7, the `Prisma` namespace comes from your generated Client (`../../generated/prisma/client`), the same import path you used for the `Article` type in part one. Importing it from `@prisma/client` no longer works. 2. The exception filter extends the `BaseExceptionFilter` class from the NestJS core package. This class provides a default implementation for the `catch` method that returns an "Internal server error" response to the user. You can learn more about this [in the NestJS docs](https://docs.nestjs.com/exception-filters#inheritance). 3. You added a `console.error` statement to log the error message to the console. This is useful for debugging purposes. -Prisma throws the `PrismaClientKnownRequestError` for many different kinds of errors. So you will need to figure out how to extract the error code from the `PrismaClientKnownRequestError` exception. The `PrismaClientKnownRequestError` exception has a `code` property that contains the error code. You can find the list of error codes in the [Prisma Error Message reference](https://www.prisma.io/docs/orm/reference/error-reference#prisma-client-query-engine). +Prisma throws the `PrismaClientKnownRequestError` for many different kinds of errors. So you will need to figure out how to extract the error code from the `PrismaClientKnownRequestError` exception. The `PrismaClientKnownRequestError` exception has a `code` property that contains the error code. You can find the list of error codes in the [Prisma error message reference](https://www.prisma.io/docs/orm/reference/error-reference). The error code you are looking for is `P2002`, which occurs for unique constraint violations. You will now update the `catch` method to throw an HTTP `409 Conflict` response in case of this error. You will also provide a custom error message to the user. - Update your exception filter implementation like this: -```typescript -//src/prisma-client-exception.filter.ts +```ts +// src/prisma-client-exception/prisma-client-exception.filter.ts -+import { ArgumentsHost, Catch, HttpStatus } from '@nestjs/common'; +import { ArgumentsHost, Catch, HttpStatus } from '@nestjs/common'; import { BaseExceptionFilter } from '@nestjs/core'; -import { Prisma } from '@prisma/client'; -+import { Response } from 'express'; +import { Prisma } from '../../generated/prisma/client'; +import { Response } from 'express'; @Catch(Prisma.PrismaClientKnownRequestError) export class PrismaClientExceptionFilter extends BaseExceptionFilter { catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) { console.error(exception.message); -+ const ctx = host.switchToHttp(); -+ const response = ctx.getResponse(); -+ const message = exception.message.replace(/\n/g, ''); - -+ switch (exception.code) { -+ case 'P2002': { -+ const status = HttpStatus.CONFLICT; -+ response.status(status).json({ -+ statusCode: status, -+ message: message, -+ }); -+ break; -+ } -+ default: + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const message = exception.message.replace(/\n/g, ''); + + switch (exception.code) { + case 'P2002': { + const status = HttpStatus.CONFLICT; + response.status(status).json({ + statusCode: status, + message: message, + }); + break; + } + default: // default 500 error code super.catch(exception, host); -+ break; -+ } + break; + } } } ``` + Here you are accessing the underlying framework `Response` object and directly modifying the response. By default, [express](https://expressjs.com/) is the HTTP framework used by NestJS under the hood. For any exception code besides `P2002`, you are sending the default "Internal server error" response. > **Note**: For production applications, be careful to not leak any sensitive information to the user in the error message. @@ -339,19 +331,20 @@ Now, for the `PrismaClientExceptionFilter` to come into effect, you need to appl Apply the exception filter to your entire application by updating the `main.ts` file: - - -```typescript +```ts // src/main.ts -+import { HttpAdapterHost, NestFactory } from '@nestjs/core'; +import { HttpAdapterHost, NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; -+import { PrismaClientExceptionFilter } from './prisma-client-exception.filter'; +import { ValidationPipe } from '@nestjs/common'; +import { PrismaClientExceptionFilter } from './prisma-client-exception/prisma-client-exception.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.useGlobalPipes(new ValidationPipe({ whitelist: true })); + const config = new DocumentBuilder() .setTitle('Median') .setDescription('The Median API description') @@ -361,31 +354,36 @@ async function bootstrap() { const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); -+ const { httpAdapter } = app.get(HttpAdapterHost); -+ app.useGlobalFilters(new PrismaClientExceptionFilter(httpAdapter)); + const { httpAdapter } = app.get(HttpAdapterHost); + app.useGlobalFilters(new PrismaClientExceptionFilter(httpAdapter)); - await app.listen(3000); + await app.listen(process.env.PORT ?? 3000); } bootstrap(); ``` + +The `HttpAdapterHost` is needed because the `BaseExceptionFilter` your filter extends uses the underlying HTTP adapter to generate its default responses. + Now, try making the same request to the `POST /articles` endpoint: ```json { - "title": "Let’s build a REST API with NestJS and Prisma.", + "title": "Let's build a REST API with NestJS and Prisma.", "description": "NestJS Series announcement.", "body": "NestJS is one of the hottest Node.js frameworks around. In this series, you will learn how to build a backend REST API with NestJS, Prisma, PostgreSQL and Swagger.", "published": true } ``` + This time you will get a more user-friendly error message: ```json { "statusCode": 409, - "message": "Invalid `this.prisma.article.create()` invocation in /Users/tasinishmam/my-code/median/src/articles/articles.service.ts:11:32 8 constructor(private prisma: PrismaService) {} 9 10 create(createArticleDto: CreateArticleDto) {→ 11 return this.prisma.article.create(Unique constraint failed on the fields: (`title`)" + "message": "Invalid `this.prisma.article.create()` invocation in ~/median/src/articles/articles.service.ts:11:32 8 constructor(private prisma: PrismaService) {} 9 10 create(createArticleDto: CreateArticleDto) {→ 11 return this.prisma.article.create(Unique constraint failed on the fields: (`title`)" } ``` + Since the `PrismaClientExceptionFilter` is a global filter, it can handle this particular type of error for all routes in your application. I recommend extending the exception filter implementation to handle other errors as well. For example, you can add a case to handle the `P2025` error code, which occurs when a record is not found in the database. You should return the status code `HttpStatus.NOT_FOUND` for this error. This would be useful for the `PATCH /articles/:id` and `DELETE /articles/:id` endpoints. @@ -393,20 +391,19 @@ I recommend extending the exception filter implementation to handle other errors ## Bonus: Handle Prisma exceptions with the `nestjs-prisma` package -So far, you have learned different techniques for _manually_ handling Prisma exceptions in a NestJS application. There is a dedicated package for using Prisma with NestJS called [`nestjs-prisma`](https://github.com/notiz-dev/nestjs-prisma) that you can also use to handle Prisma exceptions. This package is an excellent option to consider because it removes a lot of boilerplate code. +So far, you have learned different techniques for _manually_ handling Prisma exceptions in a NestJS application. There is a dedicated package for using Prisma with NestJS called [`nestjs-prisma`](https://github.com/notiz-dev/nestjs-prisma) that you can also use to handle Prisma exceptions. This package is an excellent option to consider because it removes a lot of boilerplate code, and current versions support NestJS 11 and Prisma 7. Instructions on installing and using the package are available in the [`nestjs-prisma` documentation](https://nestjs-prisma.dev/docs/installation/). When using this package, you will not need to manually create a separate `prisma` module and service, as this package will automatically make them for you. -You can learn how to use the package to handle Prisma exceptions in the [Exception Filter](https://nestjs-prisma.dev/docs/exception-filter/) section of the documentation. In a future chapter of this tutorial, we will cover the `nestjs-prisma` package in more detail. - +You can learn how to use the package to handle Prisma exceptions in the [Exception Filter](https://nestjs-prisma.dev/docs/exception-filter/) section of the documentation. ## Summary and final remarks Congratulations! You took an existing NestJS application in this tutorial and learned how to integrate error handling. You learned two different ways to handle errors: directly in your application code and by creating an exception filter. -In this chapter you learned how to handle Prisma errors. But the techniques themselves are not limited to Prisma. You can use them to handle any type of error in your application. +In this chapter you learned how to handle Prisma errors, like the `P2002` unique constraint violation. But the techniques themselves are not limited to Prisma. You can use them to handle any type of error in your application. -If you're continuing the series, the [Prisma getting started guide](https://www.prisma.io/docs/getting-started), [Prisma Migrate docs](https://www.prisma.io/docs/orm/prisma-migrate/getting-started), and [Prisma Postgres](https://www.prisma.io/postgres) are the best next resources for taking the app from tutorial to production-ready workflow. +In the [next part](/nestjs-prisma-relational-data-7D056s1kOabc) of this series, you will add a `User` model and learn how to handle relational data in your API. If you want to go deeper on the Prisma side first, the [Prisma getting started guide](https://www.prisma.io/docs/getting-started), [Prisma Migrate docs](https://www.prisma.io/docs/orm/prisma-migrate/getting-started), and [Prisma Postgres](https://www.prisma.io/postgres) are the best next resources for taking the app from tutorial to production-ready workflow. -You can find the finished code for this tutorial in the [`end-error-handling-part-3`](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma/tree/end-error-handling-part-3) branch of the [GitHub repository](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma). Please feel free to raise an issue in the repository or submit a PR if you notice a problem. You can also reach out to me directly on [Twitter](https://twitter.com/tasinishmam). +Looking ahead: [Prisma Next](https://www.prisma.io/docs/orm) is a TypeScript-native rewrite of Prisma ORM, built for AI coding agents and currently in early access. It becomes Prisma 8 at general availability; until then, Prisma 7 stays the production choice. To try it, run `npm create prisma@next` or read the [early access docs](https://pris.ly/pn-ea). diff --git a/apps/blog/content/blog/nestjs-prisma-relational-data-7D056s1kOabc/index.mdx b/apps/blog/content/blog/nestjs-prisma-relational-data-7D056s1kOabc/index.mdx index 7d3c81e7a9..a30ad0e277 100644 --- a/apps/blog/content/blog/nestjs-prisma-relational-data-7D056s1kOabc/index.mdx +++ b/apps/blog/content/blog/nestjs-prisma-relational-data-7D056s1kOabc/index.mdx @@ -2,10 +2,11 @@ title: "Building a REST API with NestJS and Prisma: Handling Relational Data" slug: "nestjs-prisma-relational-data-7D056s1kOabc" date: "2023-03-23" +updatedAt: "2026-07-08" authors: - "Tasin Ishmam" -metaTitle: "Building a REST API with NestJS and Prisma: Handling Relational Data" -metaDescription: "In this tutorial, you will learn how to handle relational data in a REST API built with NestJS, Prisma and PostgreSQL. You will create a new User model and learn how to model relations in the data layer and API layer." +metaTitle: "Handling Relational Data in a REST API with NestJS and Prisma 7" +metaDescription: "Learn how to handle relational data in a REST API built with NestJS and Prisma ORM 7: model a one-to-many relation, build user CRUD endpoints, and exclude sensitive fields from responses." metaImagePath: "/nestjs-prisma-relational-data-7D056s1kOabc/imgs/hero-2d5ec1b2f38575e64a7ae28b6915e68d77e197f2-1920x1080.png" heroImagePath: "/nestjs-prisma-relational-data-7D056s1kOabc/imgs/hero-2d5ec1b2f38575e64a7ae28b6915e68d77e197f2-1920x1080.png" heroImageAlt: "Building a REST API with NestJS and Prisma: Handling Relational Data" @@ -16,17 +17,18 @@ tags: ---
- 8 min read + 10 min read
- Welcome to the fourth tutorial in this series about building a REST API with NestJS, Prisma and PostgreSQL! - In this tutorial, you will learn how to handle relational data in your NestJS REST API. + Welcome to the fourth tutorial in this series about building a REST API with NestJS and Prisma! In this tutorial, you will learn how to handle relational data: you will model a one-to-many relation between users and articles in the Prisma schema, expose it through the API, and keep sensitive fields like passwords out of your responses. + +> **Updated (July 2026):** This tutorial has been fully revised for **Prisma ORM 7** and **NestJS 11**, and continues from the updated [third part](/nestjs-prisma-error-handling-7D056s1kOop2) of the series. Types and the `Prisma` namespace are imported from your generated Client (`generated/prisma/client`) instead of `@prisma/client`, and the code now passes TypeScript's `strictNullChecks`, which NestJS 11 enables by default. Every command and code block below was run end-to-end against `prisma@7.8`, `@prisma/client@7.8` and `@nestjs/core@11`. ## Table Of Contents -- [Introduction](#introduction)` +- [Introduction](#introduction) - [Development environment](#development-environment) - - [Clone the repository](#clone-the-repository) + - [Set up the project](#set-up-the-project) - [Project structure and files](#project-structure-and-files) - [Add a `User` model to the database](#add-a-user-model-to-the-database) - [Update your seed script](#update-your-seed-script) @@ -44,105 +46,88 @@ tags: ## Introduction -In the [first chapter](/nestjs-prisma-rest-api-7D056s1BmOL0) of this series, you created a new NestJS project and integrated it with Prisma, PostgreSQL and Swagger. Then, you built a rudimentary REST API for the backend of a blog application. In the [second chapter](/nestjs-prisma-validation-7D056s1kOla1), you learned how to do input validation and transformation. +In the [first chapter](/nestjs-prisma-rest-api-7D056s1BmOL0) of this series, you created a new NestJS project and integrated it with Prisma ORM, PostgreSQL and Swagger. Then, you built a rudimentary REST API for the backend of a blog application. In the [second chapter](/nestjs-prisma-validation-7D056s1kOla1), you learned how to do input validation and transformation, and in the [third chapter](/nestjs-prisma-error-handling-7D056s1kOop2) you added error handling. In this chapter, you will learn how to handle relational data in your data layer and API layer. -1. First, you will add a `User` model to your database schema which will have a one-to-many relationship `Article` records (i.e. one user can have multiple articles). +1. First, you will add a `User` model to your database schema which will have a one-to-many relationship with `Article` records (i.e. one user can have multiple articles). 2. Next, you will implement the API routes for the `User` endpoints to perform CRUD (create, read, update and delete) operations on `User` records. -2. Finally, you will learn how to model the `User-Article` relation in your API layer. - - -In this tutorial, you will use the REST API built in the [second chapter](/nestjs-prisma-validation-7D056s1kOla1). - +3. Finally, you will learn how to model the `User-Article` relation in your API layer. +This tutorial continues from the end of the [third chapter](/nestjs-prisma-error-handling-7D056s1kOop2). ### Development environment To follow along with this tutorial, you will be expected to: -- ... have [Node.js](https://nodejs.org) installed. -- ... have [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/install/#compose-installation-scenarios) installed. If you are using Linux, please make sure your Docker version is 20.10.0 or higher. You can check your Docker version by running `docker version` in the terminal. -- ... _optionally_ have the [Prisma VS Code Extension](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma) installed. The Prisma VS Code extension adds some really nice IntelliSense and syntax highlighting for Prisma. -- ... _optionally_ have access to a Unix shell (like the terminal/shell in Linux and macOS) to run the commands provided in this series. +- ... have [Node.js](https://nodejs.org) (v18 or higher) installed. +- ... have a PostgreSQL database. The easiest option is [Prisma Postgres](https://www.prisma.io/postgres); you can also run Postgres locally with [Docker](https://www.docker.com/) or a native install. +- ... have the [Prisma VSCode Extension](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma) installed. _(optional)_ +- ... have access to a Unix shell (like the terminal/shell in Linux and macOS) to run the commands provided in this series. _(optional)_ -If you don't have a Unix shell (for example, you are on a Windows machine), you can still follow along, but the shell commands may need to be modified for your machine. +> **Note**: If you don't have a Unix shell (for example, you are on a Windows machine), you can still follow along, but the shell commands may need to be modified for your machine. -### Clone the repository +### Set up the project -The starting point for this tutorial is the ending of [chapter two](/nestjs-prisma-validation-7D056s1kOla1) of this series. It contains a rudimentary REST API built with NestJS. +The starting point for this tutorial is the ending of [part three](/nestjs-prisma-error-handling-7D056s1kOop2) of this series: the Median REST API with input validation and error handling in place. If you're jumping in here, work through the earlier parts first; each one builds directly on the previous. -The starting point for this tutorial is available in the [`end-validation`](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma/tree/end-validation) branch of the [GitHub repository](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma). To get started, clone the repository and checkout the `end-validation` branch: +Before continuing, make sure the project runs: + +1. Start the development server: ```shell -git clone -b end-validation git@github.com:prisma/blog-backend-rest-api-nestjs-prisma.git +npm run start:dev ``` -Now, perform the following actions to get started: - -1. Navigate to the cloned directory: - ```shell cd blog-backend-rest-api-nestjs-prisma - ``` -2. Install dependencies: - ```shell npm install - ``` -3. Start the PostgreSQL database with Docker: - ```shell docker-compose up -d - ``` -4. Apply database migrations: - ```shell npx prisma migrate dev - ``` -5. Start the project: - ```shell npm run start:dev - ``` - -> **Note**: Step 4 will also generate Prisma Client and seed the database. - -Now, you should be able to access the API documentation at [`http://localhost:3000/api/`](http://localhost:3000/api/). + +2. Confirm the API documentation is available at [`http://localhost:3000/api/`](http://localhost:3000/api/). + +> **Note**: The original edition of this series linked to a companion GitHub repository. That repository still targets the older Prisma 4 and NestJS 8 stack, so for the updated series you should continue from your own project. ### Project structure and files -The repository you cloned should have the following structure: +Your project should have the following structure: + ``` median ├── node_modules + ├── generated + │ └── prisma ├── prisma │ ├── migrations │ ├── schema.prisma │ └── seed.ts ├── src - │ ├── app.controller.spec.ts - │ ├── app.controller.ts │ ├── app.module.ts - │ ├── app.service.ts │ ├── main.ts │ ├── articles - │ └── prisma + │ ├── prisma + │ └── prisma-client-exception ├── test - │   ├── app.e2e-spec.ts - │   └── jest-e2e.json + │ ├── app.e2e-spec.ts + │ └── jest-e2e.json ├── README.md ├── .env - ├── docker-compose.yml + ├── eslint.config.mjs ├── nest-cli.json ├── package-lock.json ├── package.json + ├── prisma.config.ts ├── tsconfig.build.json └── tsconfig.json ``` -> **Note**: You might notice that this folder comes with a `test` directory as well. Testing won't be covered in this tutorial. However, if you want to learn about how best practices for testing your applications with Prisma, be sure to check out this tutorial series: [The Ultimate Guide to Testing with Prisma](https://www.prisma.io/blog/series/testing-with-prisma) + +> **Note**: You might notice that this folder comes with a `test` directory as well. Testing won't be covered in this tutorial. However, if you want to learn about best practices for testing your applications with Prisma, be sure to check out this tutorial series: [The Ultimate Guide to Testing with Prisma](https://www.prisma.io/blog/series/testing-with-prisma) The notable files and directories in this repository are: -- The `src` directory contains the source code for the application. There are three modules: +- The `src` directory contains the source code for the application. There are four modules: - The `app` module is situated in the root of the `src` directory and is the entry point of the application. It is responsible for starting the web server. - - The `prisma` module contains Prisma Client, your interface to the database. + - The `prisma` module contains `PrismaService`, your interface to the database. - The `articles` module defines the endpoints for the `/articles` route and accompanying business logic. -- The `prisma` folder has the following: - - The `schema.prisma` file defines the database schema. - - The `migrations` directory contains the database migration history. - - The `seed.ts` file contains a script to seed your development database with dummy data. -- The `docker-compose.yml` file defines the Docker image for your PostgreSQL database. -- The `.env` file contains the database connection string for your PostgreSQL database. + - The `prisma-client-exception` directory contains the exception filter you built in [part three](/nestjs-prisma-error-handling-7D056s1kOop2). +- The `prisma` directory contains the `schema.prisma` file (database schema), the `migrations` directory (migration history) and the `seed.ts` script. +- The `generated/prisma` directory contains the generated Prisma Client, which in Prisma 7 lives in your project instead of `node_modules`. +- The `prisma.config.ts` file is Prisma's central configuration file, and `.env` contains the `DATABASE_URL` connection string. > **Note**: For more information about these components, go through [chapter one](/nestjs-prisma-rest-api-7D056s1BmOL0) of this tutorial series. @@ -151,7 +136,7 @@ The notable files and directories in this repository are: Currently, your database schema only has a single model: `Article`. An article can be written by a registered user. So, you will add a `User` model to your database schema to reflect this relationship. -Start by updating your Prisma schema: +Start by updating your Prisma schema. The `Article` model gets two new fields, `author` and `authorId`, and the schema gets a new `User` model: ```prisma // prisma/schema.prisma @@ -164,73 +149,86 @@ model Article { published Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt -+ author User? @relation(fields: [authorId], references: [id]) -+ authorId Int? + author User? @relation(fields: [authorId], references: [id]) + authorId Int? } -+model User { -+ id Int @id @default(autoincrement()) -+ name String? -+ email String @unique -+ password String -+ createdAt DateTime @default(now()) -+ updatedAt DateTime @updatedAt -+ articles Article[] -+} +model User { + id Int @id @default(autoincrement()) + name String? + email String @unique + password String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + articles Article[] +} ``` -The `User` model has a few fields that you might expect, like `id`, `email`, `password`, etc. It also has a one to many relationship with the `Article` model. This means that a user can have many articles, but an article can only have one author. For simplicity, the `author` relation is made optional, so it's still possible to create an article without an author. + +The `User` model has a few fields that you might expect, like `id`, `email`, `password`, etc. It also has a one-to-many relationship with the `Article` model. This means that a user can have many articles, but an article can only have one author. For simplicity, the `author` relation is made optional, so it's still possible to create an article without an author. Now, to apply the changes to your database, run the migration command: ```shell -npx prisma migrate dev --name "add-user-model" +npx prisma migrate dev --name add-user-model ``` + If the migration runs successfully, you should see the following output: + ``` ... The following migration(s) have been created and applied from new schema changes: migrations/ - └─ 20230318100533_add_user_model/ + └─ 20260708083731_add_user_model/ └─ migration.sql Your database is now in sync with your schema. -... ``` + +After the migration, regenerate Prisma Client so it knows about the new `User` model: + +```shell +npx prisma generate +``` + +> **Note**: In Prisma 7, `migrate dev` applies the migration but does not regenerate the Client into your configured `output` folder. If you skip `npx prisma generate`, the new `prisma.user` API won't exist on your Client and the seed script below fails with `TypeError: Cannot read properties of undefined (reading 'upsert')`. + ### Update your seed script The seed script is responsible for populating your database with dummy data. You will update the seed script to create a few users in your database. -Open the `prisma/seed.ts` file and update it as follows: +Open the `prisma/seed.ts` file and update the `main` function as follows: ```ts +// prisma/seed.ts + async function main() { // create two dummy users -+ const user1 = await prisma.user.upsert({ -+ where: { email: 'sabin@adams.com' }, -+ update: {}, -+ create: { -+ email: 'sabin@adams.com', -+ name: 'Sabin Adams', -+ password: 'password-sabin', -+ }, -+ }); - -+ const user2 = await prisma.user.upsert({ -+ where: { email: 'alex@ruheni.com' }, -+ update: {}, -+ create: { -+ email: 'alex@ruheni.com', -+ name: 'Alex Ruheni', -+ password: 'password-alex', -+ }, -+ }); + const user1 = await prisma.user.upsert({ + where: { email: 'sabin@adams.com' }, + update: {}, + create: { + email: 'sabin@adams.com', + name: 'Sabin Adams', + password: 'password-sabin', + }, + }); + + const user2 = await prisma.user.upsert({ + where: { email: 'alex@ruheni.com' }, + update: {}, + create: { + email: 'alex@ruheni.com', + name: 'Alex Ruheni', + password: 'password-alex', + }, + }); // create three dummy articles const post1 = await prisma.article.upsert({ where: { title: 'Prisma Adds Support for MongoDB' }, update: { -+ authorId: user1.id, + authorId: user1.id, }, create: { title: 'Prisma Adds Support for MongoDB', @@ -238,14 +236,14 @@ async function main() { description: "We are excited to share that today's Prisma ORM release adds stable support for MongoDB!", published: false, -+ authorId: user1.id, + authorId: user1.id, }, }); const post2 = await prisma.article.upsert({ where: { title: "What's new in Prisma? (Q1/22)" }, update: { -+ authorId: user2.id, + authorId: user2.id, }, create: { title: "What's new in Prisma? (Q1/22)", @@ -253,7 +251,7 @@ async function main() { description: 'Learn about everything in the Prisma ecosystem and community from January to March 2022.', published: true, -+ authorId: user2.id, + authorId: user2.id, }, }); @@ -272,31 +270,34 @@ async function main() { console.log({ user1, user2, post1, post2, post3 }); } ``` + The seed script now creates two users and three articles. The first article is written by the first user, the second article is written by the second user, and the third article is written by no one. > **Note**: At the moment, you are storing passwords in plain text. You should never do this in a real application. You will learn more about salting passwords and hashing them in the next chapter. - To execute the seed script, run the following command: ```shell npx prisma db seed ``` + If the seed script runs successfully, you should see the following output: + ``` ... 🌱 The seed command has been executed. ``` + ### Add an `authorId` field to `ArticleEntity` -After running the migration, you might have noticed a new TypeScript error. The `ArticleEntity` class `implements` the `Article` type generated by Prisma. The `Article` type has a new `authorId` field, but the `ArticleEntity` class does not have that field defined. TypeScript recognizes this mismatch in types and is raising an error. You will fix this error by adding the `authorId` field to the `ArticleEntity` class. +After regenerating the Client, you might have noticed a new TypeScript error. The `ArticleEntity` class `implements` the `Article` type generated by Prisma. The `Article` type has a new `authorId` field, but the `ArticleEntity` class does not have that field defined. TypeScript recognizes this mismatch in types and is raising an error. You will fix this error by adding the `authorId` field to the `ArticleEntity` class. Inside `ArticleEntity` add a new `authorId` field: ```ts // src/articles/entities/article.entity.ts -import { Article } from '@prisma/client'; +import { Article } from '../../../generated/prisma/client'; import { ApiProperty } from '@nestjs/swagger'; export class ArticleEntity implements Article { @@ -321,18 +322,18 @@ export class ArticleEntity implements Article { @ApiProperty() updatedAt: Date; -+ @ApiProperty({ required: false, nullable: true }) -+ authorId: number | null; + @ApiProperty({ required: false, nullable: true }) + authorId: number | null; } ``` + In a weakly typed language like JavaScript, you would have to identify and fix things like this yourself. One of the big advantages of having a strongly typed language like TypeScript is that it can quickly help you catch type-related issues. ## Implement CRUD endpoints for Users - In this section, you will implement the `/users` resource in your REST API. This will allow you to perform CRUD operations on the users in your database. -> **Note**: The content of this section will be similar to the contents of [Implement CRUD operations for Article model](nestjs-prisma-rest-api-7D056s1BmOL0#implement-crud-operations-for-article-model) section in the first chapter of this series. That section covers the topic more in-depth, so you can read it for better conceptual understanding. +> **Note**: The content of this section will be similar to the contents of the [Implement CRUD operations for Article model](/nestjs-prisma-rest-api-7D056s1BmOL0#implement-crud-operations-for-article-model) section in the first chapter of this series. That section covers the topic more in-depth, so you can read it for better conceptual understanding. ### Generate new `users` REST resource @@ -341,6 +342,7 @@ To generate a new REST resource for `users` run the following command: ```shell npx nest generate resource ``` + You will be given a few CLI prompts. Answer the questions accordingly: 1. `What name would you like to use for this resource (plural, e.g., "users")?` **users** @@ -361,18 +363,20 @@ To access `PrismaClient` inside the `Users` module, you must add the `PrismaModu ```ts // src/users/users.module.ts + import { Module } from '@nestjs/common'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; -+import { PrismaModule } from 'src/prisma/prisma.module'; +import { PrismaModule } from '../prisma/prisma.module'; @Module({ controllers: [UsersController], -+ providers: [UsersService], -+ imports: [PrismaModule], + providers: [UsersService], + imports: [PrismaModule], }) export class UsersModule {} ``` + You can now inject the `PrismaService` inside the `UsersService` and use it to access the database. To do this, add a constructor to `users.service.ts` like this: ```ts @@ -381,24 +385,25 @@ You can now inject the `PrismaService` inside the `UsersService` and use it to a import { Injectable } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; -+import { PrismaService } from 'src/prisma/prisma.service'; +import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class UsersService { -+ constructor(private prisma: PrismaService) {} + constructor(private prisma: PrismaService) {} -// CRUD operations + // CRUD operations } ``` + ### Define the `User` entity and DTO classes Just like `ArticleEntity`, you are going to define a `UserEntity` class that will be used to represent the `User` entity in the API layer. Define the `UserEntity` class in the `user.entity.ts` file as follows: - ```ts // src/users/entities/user.entity.ts + import { ApiProperty } from '@nestjs/swagger'; -import { User } from '@prisma/client'; +import { User } from '../../../generated/prisma/client'; export class UserEntity implements User { @ApiProperty() @@ -410,8 +415,8 @@ export class UserEntity implements User { @ApiProperty() updatedAt: Date; - @ApiProperty() - name: string; + @ApiProperty({ required: false, nullable: true }) + name: string | null; @ApiProperty() email: string; @@ -419,6 +424,9 @@ export class UserEntity implements User { password: string; } ``` + +Notice that `name` is typed as `string | null` because the field is optional in the Prisma schema. Typing it this way keeps the class compatible with the generated `User` type under `strictNullChecks`. + The `@ApiProperty` decorator is used to make properties visible to Swagger. Notice that you did not add the `@ApiProperty` decorator to the `password` field. This is because this field is sensitive, and you do not want to expose it in your API. > **Note**: Omitting the `@ApiProperty` decorator will only hide the `password` property from the Swagger documentation. The property will still be visible in the response body. You will handle this issue in a later section. @@ -449,6 +457,7 @@ export class CreateUserDto { password: string; } ``` + `@IsString`, `@MinLength` and `@IsNotEmpty` are validation decorators that will be used to validate the data sent to the API. Validation is covered in more detail in the [second chapter](/nestjs-prisma-validation-7D056s1kOla1) of this series. The definition of `UpdateUserDto` is automatically inferred from the `CreateUserDto` definition, so it does not need to be defined explicitly. @@ -463,33 +472,34 @@ The `UsersService` is responsible for modifying and fetching data from the datab import { Injectable } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class UsersService { constructor(private prisma: PrismaService) {} create(createUserDto: CreateUserDto) { -+ return this.prisma.user.create({ data: createUserDto }); + return this.prisma.user.create({ data: createUserDto }); } findAll() { -+ return this.prisma.user.findMany(); + return this.prisma.user.findMany(); } findOne(id: number) { -+ return this.prisma.user.findUnique({ where: { id } }); + return this.prisma.user.findUnique({ where: { id } }); } update(id: number, updateUserDto: UpdateUserDto) { -+ return this.prisma.user.update({ where: { id }, data: updateUserDto }); + return this.prisma.user.update({ where: { id }, data: updateUserDto }); } remove(id: number) { -+ return this.prisma.user.delete({ where: { id } }); + return this.prisma.user.delete({ where: { id } }); } } ``` + ### Define the `UsersController` class The `UsersController` is responsible for handling requests and responses to the `users` endpoints. It will leverage the `UsersService` to access the database, the `UserEntity` to define the response body and the `CreateUserDto` and `UpdateUserDto` to define the request body. @@ -515,56 +525,62 @@ import { Patch, Param, Delete, -+ ParseIntPipe, + ParseIntPipe, + NotFoundException, } from '@nestjs/common'; import { UsersService } from './users.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; -+import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger'; -+import { UserEntity } from './entities/user.entity'; +import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { UserEntity } from './entities/user.entity'; @Controller('users') -+@ApiTags('users') +@ApiTags('users') export class UsersController { constructor(private readonly usersService: UsersService) {} @Post() -+ @ApiCreatedResponse({ type: UserEntity }) + @ApiCreatedResponse({ type: UserEntity }) create(@Body() createUserDto: CreateUserDto) { return this.usersService.create(createUserDto); } @Get() -+ @ApiOkResponse({ type: UserEntity, isArray: true }) + @ApiOkResponse({ type: UserEntity, isArray: true }) findAll() { return this.usersService.findAll(); } @Get(':id') -+ @ApiOkResponse({ type: UserEntity }) -+ findOne(@Param('id', ParseIntPipe) id: number) { -+ return this.usersService.findOne(id); + @ApiOkResponse({ type: UserEntity }) + async findOne(@Param('id', ParseIntPipe) id: number) { + const user = await this.usersService.findOne(id); + if (!user) { + throw new NotFoundException(`User with ${id} does not exist.`); + } + return user; } @Patch(':id') -+ @ApiCreatedResponse({ type: UserEntity }) -+ update( -+ @Param('id', ParseIntPipe) id: number, -+ @Body() updateUserDto: UpdateUserDto, + @ApiOkResponse({ type: UserEntity }) + update( + @Param('id', ParseIntPipe) id: number, + @Body() updateUserDto: UpdateUserDto, ) { -+ return this.usersService.update(id, updateUserDto); + return this.usersService.update(id, updateUserDto); } @Delete(':id') -+ @ApiOkResponse({ type: UserEntity }) -+ remove(@Param('id', ParseIntPipe) id: number) { -+ return this.usersService.remove(id); + @ApiOkResponse({ type: UserEntity }) + remove(@Param('id', ParseIntPipe) id: number) { + return this.usersService.remove(id); } } ``` -The updated controller uses the `@ApiTags` decorator to group the endpoints under the `users` tag. It also uses the `@ApiCreatedResponse` and `@ApiOkResponse` decorators to define the response body for each endpoint. -The updated Swagger [API page](http://localhost:3000/api) should look like this +The updated controller uses the `@ApiTags` decorator to group the endpoints under the `users` tag. It also uses the `@ApiCreatedResponse` and `@ApiOkResponse` decorators to define the response body for each endpoint. The `findOne` handler additionally throws a `NotFoundException` when no user exists with the given `id`, following the error handling pattern from [part three](/nestjs-prisma-error-handling-7D056s1kOop2); this also satisfies `strictNullChecks`, since `findOne` in the service can return `null`. + +The updated Swagger [API page](http://localhost:3000/api) should look like this: ![Updated swagger page](/nestjs-prisma-relational-data-7D056s1kOabc/imgs/users-crud-2.png) @@ -581,7 +597,6 @@ You have two options to fix this issue: 1. Manually remove the password from the response body in the controller route handlers 2. Use an [interceptor](https://docs.nestjs.com/interceptors) to automatically remove the password from the response body - The first option is error prone and results in unnecessary code duplication. So, you will use the second method. ### Use the `ClassSerializerInterceptor` to remove a field from the response @@ -595,40 +610,45 @@ First, enable `ClassSerializerInterceptor` globally by updating `main.ts`: ```ts // src/main.ts -+import { NestFactory, Reflector } from '@nestjs/core'; +import { HttpAdapterHost, NestFactory, Reflector } from '@nestjs/core'; import { AppModule } from './app.module'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; -+import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common'; +import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common'; +import { PrismaClientExceptionFilter } from './prisma-client-exception/prisma-client-exception.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe({ whitelist: true })); -+ app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); + app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); const config = new DocumentBuilder() .setTitle('Median') .setDescription('The Median API description') .setVersion('0.1') .build(); + const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); - await app.listen(3000); + const { httpAdapter } = app.get(HttpAdapterHost); + app.useGlobalFilters(new PrismaClientExceptionFilter(httpAdapter)); + + await app.listen(process.env.PORT ?? 3000); } bootstrap(); ``` + > **Note:** It's also possible to bind an interceptor to a method or controller instead of globally. You can read more about it in the [NestJS documentation](https://docs.nestjs.com/interceptors#binding-interceptors). The `ClassSerializerInterceptor` uses the `class-transformer` package to define how to transform objects. Use the `@Exclude()` decorator to exclude the `password` field in the `UserEntity` class: - ```ts // src/users/entities/user.entity.ts import { ApiProperty } from '@nestjs/swagger'; -import { User } from '@prisma/client'; -+import { Exclude } from 'class-transformer'; +import { User } from '../../../generated/prisma/client'; +import { Exclude } from 'class-transformer'; export class UserEntity implements User { @ApiProperty() @@ -640,17 +660,18 @@ export class UserEntity implements User { @ApiProperty() updatedAt: Date; - @ApiProperty() - name: string; + @ApiProperty({ required: false, nullable: true }) + name: string | null; @ApiProperty() email: string; -+ @Exclude() + @Exclude() password: string; } ``` -If you try using the `GET /users/:id` endpoint again, you'll notice that the `password` field is still being exposed 🤔. This is because, currently the route handlers in your controller returns the `User` type generated by Prisma Client. The `ClassSerializerInterceptor` only works with classes decorated with the `@Exclude()` decorator. In this case, it's the `UserEntity` class. So, you need to update the route handlers to return the `UserEntity` type instead. + +If you try using the `GET /users/:id` endpoint again, you'll notice that the `password` field is still being exposed 🤔. This is because, currently the route handlers in your controller return the `User` type generated by Prisma Client. The `ClassSerializerInterceptor` only works with classes decorated with the `@Exclude()` decorator. In this case, it's the `UserEntity` class. So, you need to update the route handlers to return the `UserEntity` type instead. First, you need to create a constructor that will instantiate a `UserEntity` object. @@ -658,13 +679,13 @@ First, you need to create a constructor that will instantiate a `UserEntity` obj // src/users/entities/user.entity.ts import { ApiProperty } from '@nestjs/swagger'; -import { User } from '@prisma/client'; +import { User } from '../../../generated/prisma/client'; import { Exclude } from 'class-transformer'; export class UserEntity implements User { -+ constructor(partial: Partial) { -+ Object.assign(this, partial); -+ } + constructor(partial: Partial) { + Object.assign(this, partial); + } @ApiProperty() id: number; @@ -675,8 +696,8 @@ export class UserEntity implements User { @ApiProperty() updatedAt: Date; - @ApiProperty() - name: string; + @ApiProperty({ required: false, nullable: true }) + name: string | null; @ApiProperty() email: string; @@ -685,10 +706,10 @@ export class UserEntity implements User { password: string; } ``` -The constructor takes an object and uses the `Object.assign()` method to copy the properties from the `partial` object to the `UserEntity` instance. The type of `partial` is `Partial`. This means that the `partial` object can contain any subset of the properties defined in the `UserEntity` class. -Next, update the `UsersController` route handlers to return `UserEntity` instead of `Prisma.User` objects: +The constructor takes an object and uses the `Object.assign()` method to copy the properties from the `partial` object to the `UserEntity` instance. The type of `partial` is `Partial`. This means that the `partial` object can contain any subset of the properties defined in the `UserEntity` class. +Next, update the `UsersController` route handlers to return `UserEntity` instead of Prisma `User` objects: ```ts // src/users/users.controller.ts @@ -700,39 +721,44 @@ export class UsersController { @Post() @ApiCreatedResponse({ type: UserEntity }) -+ async create(@Body() createUserDto: CreateUserDto) { -+ return new UserEntity(await this.usersService.create(createUserDto)); + async create(@Body() createUserDto: CreateUserDto) { + return new UserEntity(await this.usersService.create(createUserDto)); } @Get() @ApiOkResponse({ type: UserEntity, isArray: true }) -+ async findAll() { -+ const users = await this.usersService.findAll(); -+ return users.map((user) => new UserEntity(user)); + async findAll() { + const users = await this.usersService.findAll(); + return users.map((user) => new UserEntity(user)); } @Get(':id') @ApiOkResponse({ type: UserEntity }) -+ async findOne(@Param('id', ParseIntPipe) id: number) { -+ return new UserEntity(await this.usersService.findOne(id)); + async findOne(@Param('id', ParseIntPipe) id: number) { + const user = await this.usersService.findOne(id); + if (!user) { + throw new NotFoundException(`User with ${id} does not exist.`); + } + return new UserEntity(user); } @Patch(':id') - @ApiCreatedResponse({ type: UserEntity }) -+ async update( + @ApiOkResponse({ type: UserEntity }) + async update( @Param('id', ParseIntPipe) id: number, @Body() updateUserDto: UpdateUserDto, ) { -+ return new UserEntity(await this.usersService.update(id, updateUserDto)); + return new UserEntity(await this.usersService.update(id, updateUserDto)); } @Delete(':id') @ApiOkResponse({ type: UserEntity }) -+ async remove(@Param('id', ParseIntPipe) id: number) { -+ return new UserEntity(await this.usersService.remove(id)); + async remove(@Param('id', ParseIntPipe) id: number) { + return new UserEntity(await this.usersService.remove(id)); } } ``` + Now, the password should be omitted from the response object. ![`GET /users/:id` does not reveal password](/nestjs-prisma-relational-data-7D056s1kOabc/imgs/password-omitted.png) @@ -748,28 +774,27 @@ The data access logic is implemented inside the `ArticlesService`. Update the `f // src/articles/articles.service.ts findOne(id: number) { -+ return this.prisma.article.findUnique({ -+ where: { id }, -+ include: { -+ author: true, -+ }, -+ }); + return this.prisma.article.findUnique({ + where: { id }, + include: { + author: true, + }, + }); } ``` + If you test the `GET /articles/:id` endpoint, you'll notice that the author of an article, if present, is included in the response object. However, there's a problem. The `password` field is exposed again 🤦. ![`GET /articles/:id` reveals password](/nestjs-prisma-relational-data-7D056s1kOabc/imgs/password-revealed-get-articles.png) The reason for this issue is very similar to last time. Currently, the `ArticlesController` returns instances of Prisma generated types, whereas the `ClassSerializerInterceptor` works with the `UserEntity` class. To fix this, you will update the implementation of the `ArticleEntity` class and make sure it initializes the `author` property with an instance of `UserEntity`. - - ```ts // src/articles/entities/article.entity.ts -import { Article } from '@prisma/client'; +import { Article } from '../../../generated/prisma/client'; import { ApiProperty } from '@nestjs/swagger'; -+import { UserEntity } from 'src/users/entities/user.entity'; +import { UserEntity } from '../../users/entities/user.entity'; export class ArticleEntity implements Article { @ApiProperty() @@ -796,25 +821,28 @@ export class ArticleEntity implements Article { @ApiProperty({ required: false, nullable: true }) authorId: number | null; -+ @ApiProperty({ required: false, type: UserEntity }) -+ author?: UserEntity; + @ApiProperty({ required: false, type: UserEntity }) + author?: UserEntity | null; -+ constructor({ author, ...data }: Partial) { -+ Object.assign(this, data); + constructor({ author, ...data }: Partial) { + Object.assign(this, data); -+ if (author) { -+ this.author = new UserEntity(author); -+ } -+ } + if (author) { + this.author = new UserEntity(author); + } + } } ``` + Once again, you are using the `Object.assign()` method to copy the properties from the `data` object to the `ArticleEntity` instance. The `author` property, if it is present, is initialized as an instance of `UserEntity`. +Note the type of the `author` property: `UserEntity | null`. When you query an article with `include: { author: true }`, Prisma types the relation as `User | null` because the relation is optional. Declaring the property as nullable keeps the constructor argument compatible under `strictNullChecks`; with a plain `author?: UserEntity`, the code no longer compiles. Now update the `ArticlesController` to return instances of `ArticleEntity` objects: ```ts // src/articles/articles.controller.ts + import { Controller, Get, @@ -824,6 +852,7 @@ import { Param, Delete, ParseIntPipe, + NotFoundException, } from '@nestjs/common'; import { ArticlesService } from './articles.service'; import { CreateArticleDto } from './dto/create-article.dto'; @@ -838,62 +867,64 @@ export class ArticlesController { @Post() @ApiCreatedResponse({ type: ArticleEntity }) -+ async create(@Body() createArticleDto: CreateArticleDto) { -+ return new ArticleEntity( -+ await this.articlesService.create(createArticleDto), -+ ); + async create(@Body() createArticleDto: CreateArticleDto) { + return new ArticleEntity( + await this.articlesService.create(createArticleDto), + ); } @Get() @ApiOkResponse({ type: ArticleEntity, isArray: true }) -+ async findAll() { -+ const articles = await this.articlesService.findAll(); -+ return articles.map((article) => new ArticleEntity(article)); + async findAll() { + const articles = await this.articlesService.findAll(); + return articles.map((article) => new ArticleEntity(article)); } @Get('drafts') @ApiOkResponse({ type: ArticleEntity, isArray: true }) -+ async findDrafts() { -+ const drafts = await this.articlesService.findDrafts(); -+ return drafts.map((draft) => new ArticleEntity(draft)); + async findDrafts() { + const drafts = await this.articlesService.findDrafts(); + return drafts.map((draft) => new ArticleEntity(draft)); } @Get(':id') @ApiOkResponse({ type: ArticleEntity }) -+ async findOne(@Param('id', ParseIntPipe) id: number) { -+ return new ArticleEntity(await this.articlesService.findOne(id)); + async findOne(@Param('id', ParseIntPipe) id: number) { + const article = await this.articlesService.findOne(id); + if (!article) { + throw new NotFoundException(`Article with ${id} does not exist.`); + } + return new ArticleEntity(article); } @Patch(':id') - @ApiCreatedResponse({ type: ArticleEntity }) -+ async update( + @ApiOkResponse({ type: ArticleEntity }) + async update( @Param('id', ParseIntPipe) id: number, @Body() updateArticleDto: UpdateArticleDto, ) { -+ return new ArticleEntity( -+ await this.articlesService.update(id, updateArticleDto), -+ ); + return new ArticleEntity( + await this.articlesService.update(id, updateArticleDto), + ); } @Delete(':id') @ApiOkResponse({ type: ArticleEntity }) -+ async remove(@Param('id', ParseIntPipe) id: number) { -+ return new ArticleEntity(await this.articlesService.remove(id)); + async remove(@Param('id', ParseIntPipe) id: number) { + return new ArticleEntity(await this.articlesService.remove(id)); } } ``` + Now, `GET /articles/:id` returns the `author` object without the `password` field: ![`GET /articles/:id` does not reveal password](/nestjs-prisma-relational-data-7D056s1kOabc/imgs/password-omitted-get-articles.png) - - - ## Summary and final remarks +In this chapter, you learned how to model relational data in a NestJS application using Prisma ORM. You also learned about the `ClassSerializerInterceptor` and how to use entity classes to control the data that is returned to the client. -In this chapter, you learned how to model relational data in a NestJS application using Prisma. You also learned about the `ClassSerializerInterceptor` and how to use entity classes to control the data that is returned to the client. - -You can find the finished code for this tutorial in the [`end-relational-data`](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma/tree/end-relational-data) branch of the [GitHub repository](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma). Please feel free to raise an issue in the repository or submit a PR if you notice a problem. You can also reach out to me directly on [Twitter](https://twitter.com/tasinishmam). +In the [next part](/nestjs-prisma-authentication-7D056s1s0k3l) of this series, you will secure these endpoints by adding JWT authentication to the API, and replace the plain text passwords with properly hashed ones. If you want to go deeper on relations first, the [Prisma relations docs](https://www.prisma.io/docs/orm/prisma-schema/data-model/relations) cover one-to-many and other relation types in depth, and [Prisma Postgres](https://www.prisma.io/postgres) is an easy way to stand up the database layer for follow-on work. +Looking ahead: [Prisma Next](https://www.prisma.io/docs/orm) is a TypeScript-native rewrite of Prisma ORM, built for AI coding agents and currently in early access. It becomes Prisma 8 at general availability; until then, Prisma 7 stays the production choice. To try it, run `npm create prisma@next` or read the [early access docs](https://pris.ly/pn-ea). diff --git a/apps/blog/content/blog/nestjs-prisma-validation-7D056s1kOla1/index.mdx b/apps/blog/content/blog/nestjs-prisma-validation-7D056s1kOla1/index.mdx index 4ca70084f9..e512172845 100644 --- a/apps/blog/content/blog/nestjs-prisma-validation-7D056s1kOla1/index.mdx +++ b/apps/blog/content/blog/nestjs-prisma-validation-7D056s1kOla1/index.mdx @@ -2,10 +2,11 @@ title: "Building a REST API with NestJS and Prisma: Input Validation & Transformation" slug: "nestjs-prisma-validation-7D056s1kOla1" date: "2022-07-19" +updatedAt: "2026-07-08" authors: - "Tasin Ishmam" -metaTitle: "Learn how to add Input Validation to a REST API with NestJS and Prisma" -metaDescription: "Learn how to build a backend REST API with NestJS, Prisma, PostgreSQL and Swagger. In this article, you will learn how to perform input validation and transformation for your API." +metaTitle: "Input Validation in a REST API with NestJS and Prisma 7" +metaDescription: "Learn how to add input validation and transformation to a NestJS REST API with ValidationPipe, class-validator and ParseIntPipe. Fully updated for Prisma ORM 7 and NestJS 11." metaImagePath: "/nestjs-prisma-validation-7D056s1kOla1/imgs/meta-c114050a8b7a4eacbd9270a105ff912a0e91eec5-1272x716.png" heroImagePath: "/nestjs-prisma-validation-7D056s1kOla1/imgs/hero-adc0af7eb5b3f3e751cd795ba7e184ce98a3ed81-844x474.svg" heroImageAlt: "Building a REST API with NestJS and Prisma: Input Validation & Transformation" @@ -19,26 +20,13 @@ tags: 8 min read - Welcome to the second tutorial on the series about building a REST API with NestJS, Prisma and PostgreSQL! - In this tutorial, you will learn how to perform input validation and transformation in your API. - -## Table Of Contents - -- [Introduction](#introduction) - - [Development environment](#development-environment) - - [Clone the repository](#clone-the-repository) - - [Project structure and files](#project-structure-and-files) -- [Perform input validation](#perform-input-validation) - - [Set up `ValidationPipe` globally](#set-up-validationpipe-globally) - - [Add validation rules to `CreateArticleDto`](#add-validation-rules-to-createarticledto) - - [Strip unnecessary properties from client requests](#strip-unnecessary-properties-from-client-requests) -- [Use `ParseIntPipe` to transform dynamic URL paths](#transform-dynamic-url-paths-with-parseintpipe) -- [Summary and final remarks](#summary-and-final-remarks) + Input validation in NestJS is handled by the built-in `ValidationPipe`: you declare rules with `class-validator` decorators on your DTO classes, register the pipe globally, and every incoming request body is checked before it reaches your route handlers. This tutorial is the second part of a five-part series on building a REST API with NestJS and Prisma ORM. In it, you will add validation and transformation to the Median blog API: reject malformed input, strip unknown fields with the `whitelist` option, and convert URL path parameters from strings to numbers with `ParseIntPipe`. +> **Updated (July 2026):** This tutorial has been fully revised for **Prisma ORM 7** and **NestJS 11**, and continues directly from the updated [first part](/nestjs-prisma-rest-api-7D056s1BmOL0) of the series. Every command and code block below was run end-to-end against `prisma@7.8`, `@prisma/client@7.8` and `@nestjs/core@11`, with the database on [Prisma Postgres](https://www.prisma.io/docs/postgres). ## Introduction -In the [first part](/nestjs-prisma-rest-api-7D056s1BmOL0) of this series, you created a new NestJS project and integrated it with Prisma, PostgreSQL and Swagger. Then, you built a rudimentary REST API for the backend of a blog application. +In the [first part](/nestjs-prisma-rest-api-7D056s1BmOL0) of this series, you created a new NestJS project and integrated it with Prisma ORM, PostgreSQL and Swagger. Then, you built a rudimentary REST API for the backend of a blog application called "Median". In this part, you will learn how to validate the input, so it conforms to your API specifications. Input validation is performed to ensure only properly formed data from the client passes through your API. It is best practice to validate the correctness of any data sent into a web application. This can help prevent malformed data and abuse of your API. @@ -46,116 +34,97 @@ You will also learn how to perform input transformation. Input transformation is ### Development environment -To follow along with this tutorial, you will be expected to have: +To follow along with this tutorial, you will be expected to: + +- ... have [Node.js](https://nodejs.org) (v18 or higher) installed. +- ... have a PostgreSQL database. The easiest option is Prisma Postgres; you can also run Postgres locally with [Docker](https://www.docker.com/) or a native install. +- ... have the [Prisma VSCode Extension](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma) installed. _(optional)_ +- ... have access to a Unix shell (like the terminal/shell in Linux and macOS) to run the commands provided in this series. _(optional)_ + +> **Note 1**: The optional Prisma VSCode extension adds IntelliSense and syntax highlighting for Prisma. -- [Node.js](https://nodejs.org/) installed. -- [Docker](https://www.docker.com/) or [PostgreSQL](https://www.postgresql.org/) installed. -- Installed the [Prisma VSCode Extension](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma). *(optional)* -- Access to a Unix shell (like the terminal/shell in Linux and macOS) to run the commands provided in this series. *(optional)* +> **Note 2**: If you don't have a Unix shell (for example, you are on a Windows machine), you can still follow along, but the shell commands may need to be modified for your machine. -> **Note**: -> -> 1. The optional Prisma VS Code extension adds some nice IntelliSense and syntax highlighting for Prisma. -> -> 2. If you don't have a Unix shell (for example, you are on a Windows machine), you can still follow along, but the shell commands may need to be modified for your machine. +### Set up the project -### Clone the repository +The starting point for this tutorial is the ending of [part one](/nestjs-prisma-rest-api-7D056s1BmOL0) of this series: a rudimentary REST API built with NestJS 11 and Prisma ORM 7, with a working database connection and seed data. If you haven't completed the first tutorial yet, work through it first; it takes about 20 minutes and every step in this article builds directly on it. -The starting point for this tutorial is the ending of [part one](/nestjs-prisma-rest-api-7D056s1BmOL0) of this series. It contains a rudimentary REST API built with NestJS. I would recommend finishing the first tutorial before starting this one. +Before continuing, make sure the project runs: -The starting point for this tutorial is available in the [begin-validation](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma/tree/begin-validation) branch of the [GitHub repository](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma). To get started, clone the repository and checkout the `begin-validation` branch: +1. Start the development server: ```shell -git clone -b begin-validation git@github.com:prisma/blog-backend-rest-api-nestjs-prisma.git +npm run start:dev ``` -Now, perform the following actions to get started: - -1. Navigate to the cloned directory: - ```shell cd blog-backend-rest-api-nestjs-prisma - ``` -2. Install dependencies: - ```shell npm install - ``` -3. Start the PostgreSQL database with docker: - ```shell docker-compose up -d - ``` -4. Apply database migrations: - ```shell npx prisma migrate dev - ``` -5. Start the project: - ```shell npm run start:dev - ``` - -> *Note*: Step 4 will also generate Prisma Client and seed the database. - -Now, you should be able to access the API documentation at [`http://localhost:3000/api/`](http://localhost:3000/api/). + +2. Confirm the API documentation is available at [`http://localhost:3000/api/`](http://localhost:3000/api/). + +> **Note**: The original edition of this series linked to a companion GitHub repository. That repository still targets the older Prisma 4 and NestJS 8 stack, so for the updated series you should continue from your own project from part one. ### Project structure and files -The repository you cloned should have the following structure: +Your project should have the following structure: + ``` median ├── node_modules + ├── generated + │ └── prisma ├── prisma │ ├── migrations │ ├── schema.prisma │ └── seed.ts ├── src - │ ├── app.controller.spec.ts - │ ├── app.controller.ts │ ├── app.module.ts - │ ├── app.service.ts │ ├── main.ts │ ├── articles │ └── prisma ├── test - │   ├── app.e2e-spec.ts - │   └── jest-e2e.json + │ ├── app.e2e-spec.ts + │ └── jest-e2e.json ├── README.md ├── .env - ├── docker-compose.yml + ├── eslint.config.mjs ├── nest-cli.json ├── package-lock.json ├── package.json + ├── prisma.config.ts ├── tsconfig.build.json └── tsconfig.json ``` + The notable files and directories in this repository are: - The `src` directory contains the source code for the application. There are three modules: - The `app` module is situated in the root of the `src` directory and is the entry point of the application. It is responsible for starting the web server. - - The `prisma` module contains the Prisma Client, your database query builder. + - The `prisma` module contains `PrismaService`, which wraps the Prisma Client, your database query builder. - The `articles` module defines the endpoints for the `/articles` route and accompanying business logic. -- The `prisma` module has the following: +- The `prisma` directory has the following: - The `schema.prisma` file defines the database schema. - The `migrations` directory contains the database migration history. - The `seed.ts` file contains a script to seed your development database with dummy data. -- The `docker-compose.yml` file defines the Docker image for your PostgreSQL database. -- The `.env` file contains the database connection string for your PostgreSQL database. +- The `generated/prisma` directory contains the generated Prisma Client. In Prisma 7, the Client is generated into your project as source files instead of into `node_modules`. +- The `prisma.config.ts` file is Prisma's central configuration file, where the database connection URL and the seed command live. +- The `.env` file contains the `DATABASE_URL` connection string for your database. > **Note**: For more information about these components, go through [part one](/nestjs-prisma-rest-api-7D056s1BmOL0) of this tutorial series. ## Perform input validation -To perform input validation, you will be using [NestJS Pipes](https://docs.nestjs.com/pipes). Pipes operate on the arguments being processed by a route handler. Nest invokes a pipe before the route handler, and the pipe receives the arguments destined for the route handler. Pipes can do a number of things, like validate the input, add fields to the input, etc. Pipes are similar to [middleware](https://docs.nestjs.com/middleware), but the scope of pipes is limited to processing input arguments. NestJS provides a few pipes out-of-the-box, but you can also create your own [custom pipes](https://docs.nestjs.com/pipes#custom-pipes). - - +To perform input validation, you will be using [NestJS Pipes](https://docs.nestjs.com/pipes). A pipe in NestJS is a class that validates or transforms the arguments of a route handler before the handler runs. Nest invokes a pipe before the route handler, and the pipe receives the arguments destined for the route handler. Pipes are similar to [middleware](https://docs.nestjs.com/middleware), but the scope of pipes is limited to processing input arguments. NestJS provides a few pipes out-of-the-box, but you can also create your own [custom pipes](https://docs.nestjs.com/pipes#custom-pipes). Pipes have two typical use cases: - **Validation**: Evaluate input data and, if valid, pass it through unchanged; otherwise, throw an exception when the data is incorrect. - **Transformation**: Transform input data to the desired form (e.g., from string to integer). -A NestJS validation pipe will check the arguments passed to a route. If the arguments are vaid, the pipe will pass the arguments to the route handler without any modification. However, if the arguments violate any of the specified validation rules, the pipe will throw an exception. +A NestJS validation pipe will check the arguments passed to a route. If the arguments are valid, the pipe will pass the arguments to the route handler without any modification. However, if the arguments violate any of the specified validation rules, the pipe will throw an exception with an HTTP `400 Bad Request` status. -The following two diagrams shows how validation pipe works, for an arbitrary `/example` route. +The following two diagrams show how a validation pipe works, for an arbitrary `/example` route. ![](/nestjs-prisma-validation-7D056s1kOla1/imgs/valid-args.png) ![](/nestjs-prisma-validation-7D056s1kOla1/imgs/invalid-args.png) - - - In this section, you will focus on the validation use case. ### Set up `ValidationPipe` globally @@ -167,35 +136,38 @@ To use this feature, you will need to add two packages to your project: ```shell npm install class-validator class-transformer ``` -The class-validator package provides decorators for validating input data, and the class-transformer package provides decorators to transform input data to the desired form. Both packages are well integrated with NestJS pipes. + +The `class-validator` package provides decorators for validating input data, and the `class-transformer` package converts incoming JSON into typed instances of your DTO classes so those rules can run. Both packages are required by the `ValidationPipe`. Now import the `ValidationPipe` in your `main.ts` file and use the `app.useGlobalPipes` method to make it available globally in your application: ```ts -diff // src/main.ts +// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; -+import { ValidationPipe } from '@nestjs/common'; +import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); -+ app.useGlobalPipes(new ValidationPipe()); + app.useGlobalPipes(new ValidationPipe()); const config = new DocumentBuilder() .setTitle('Median') .setDescription('The Median API description') .setVersion('0.1') .build(); + const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); - await app.listen(3000); + await app.listen(process.env.PORT ?? 3000); } bootstrap(); ``` + ### Add validation rules to `CreateArticleDto` You will now use the [`class-validator`](https://github.com/typestack/class-validator) package to add validation decorators to `CreateArticleDto`. You will apply the following rules to `CreateArticleDto`: @@ -206,6 +178,7 @@ You will now use the [`class-validator`](https://github.com/typestack/class-vali 4. `title`, `description` and `body` must be of type `string` and `published` must be of type `boolean`. Open the `src/articles/dto/create-article.dto.ts` file and replace its contents with the following: + ```ts // src/articles/dto/create-article.dto.ts @@ -244,8 +217,8 @@ export class CreateArticleDto { published?: boolean = false; } ``` -These rules will be picked up by the `ValidationPipe` and applied automatically to your route handlers. One of the advantages of using decorators for validation is that the `CreateArticleDto` remains the single source of truth for all arguments to the `POST /articles` endpoint. So you don't need to define a separate validation class. +These rules will be picked up by the `ValidationPipe` and applied automatically to your route handlers. One of the advantages of using decorators for validation is that the `CreateArticleDto` remains the single source of truth for all arguments to the `POST /articles` endpoint. So you don't need to define a separate validation class. Test out the validation rules you have in place. Try creating an article using the `POST /articles` endpoint with a very short placeholder `title` like this: @@ -257,19 +230,24 @@ Test out the validation rules you have in place. Try creating an article using t "published": false } ``` -You should get an HTTP 400 error response along with details in the response body about what validation rule was broken. -![HTTP 400 response with descriptive error message](/nestjs-prisma-validation-7D056s1kOla1/imgs/validation-error.png) +You should get an HTTP 400 error response along with details in the response body about what validation rule was broken: + +```json +{ + "message": ["title must be longer than or equal to 5 characters"], + "error": "Bad Request", + "statusCode": 400 +} +``` This diagram explains what the `ValidationPipe` is doing under the hood for invalid inputs to the `/articles` route: ![Input validation flow with ValidationPipe](/nestjs-prisma-validation-7D056s1kOla1/imgs/invalid-args-specific.png) - - ### Strip unnecessary properties from client requests -The `CreateArticleDTO` defines the properties that need to be sent to the `POST /articles` endpoint to create a new article. `UpdateArticleDTO` does the same, but for the `PATCH /articles/{id}` endpoint. +The `CreateArticleDto` defines the properties that need to be sent to the `POST /articles` endpoint to create a new article. `UpdateArticleDto` does the same, but for the `PATCH /articles/{id}` endpoint. Currently, for both of these endpoints it is possible to send additional properties that are not defined in the DTO. This can lead to unforeseen bugs or security issues. For example, you could manually pass invalid `createdAt` and `updatedAt` values to the `POST /articles` endpoint. Since TypeScript type information is not available at run-time, your application will not be able to identify that these fields are not available in the DTO. @@ -285,47 +263,35 @@ To give an example, try sending the following request to the `POST /articles` en "updatedAt": "2021-06-02T18:20:29.310Z" } ``` + ![](/nestjs-prisma-validation-7D056s1kOla1/imgs/inject-dates.png) In this way, you can inject invalid values. Here you have created an article that has an `updatedAt` value that precedes `createdAt`, which does not make sense. -To prevent this, you will need to filter any unnecessary fields/properties from client requests. Fortunately, NestJS provides an out-of-the-box for this as well. All you need to do is pass the `whitelist: true` option when initializing the `ValidationPipe` inside your application. +To prevent this, you will need to filter any unnecessary fields/properties from client requests. Fortunately, NestJS provides an out-of-the-box option for this as well. All you need to do is pass the `whitelist: true` option when initializing the `ValidationPipe` inside your application. ```ts -diff // src/main.ts - -import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; -import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; -import { ValidationPipe } from '@nestjs/common'; +// src/main.ts async function bootstrap() { const app = await NestFactory.create(AppModule); -+ app.useGlobalPipes(new ValidationPipe({ whitelist: true })); + app.useGlobalPipes(new ValidationPipe({ whitelist: true })); - const config = new DocumentBuilder() - .setTitle('Median') - .setDescription('The Median API description') - .setVersion('0.1') - .build(); - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('api', app, document); - - await app.listen(3000); + // ... } bootstrap(); ``` -With this option set to true, `ValidationPipe` will automatically remove all *non-whitelisted* properties, where “*non-whitelisted”* means properties without any validation decorators. It’s important to note that this option will filter *all properties* without validation decorators, *even if they are defined in the DTO.* + +With this option set to true, `ValidationPipe` will automatically remove all _non-whitelisted_ properties, where _"non-whitelisted"_ means properties without any validation decorators. It's important to note that this option will filter _all properties_ without validation decorators, _even if they are defined in the DTO._ Now, any additional fields/properties that are passed to the request will be stripped automatically by NestJS, preventing the previously shown exploit. > **Note**: The NestJS `ValidationPipe` is highly configurable. All configuration options available are documented in the [NestJS docs](https://docs.nestjs.com/techniques/validation#using-the-built-in-validationpipe). If necessary, you can also build [custom validation pipes](https://docs.nestjs.com/pipes#custom-pipes) for your application. - ## Transform dynamic URL paths with `ParseIntPipe` -Inside your API, you are currently accepting the id parameter for the `GET /articles/{id}` , `PATCH /articles/{id}` and `DELETE /articles/{id}` endpoints as a part of the path. NestJS parses the `id` parameter as a string from the URL path. Then, the string is cast to a number inside your application code before being passed to the ArticlesService. For example, take a look at the `DELETE /articles/{id}` route handler: +Inside your API, you are currently accepting the `id` parameter for the `GET /articles/{id}`, `PATCH /articles/{id}` and `DELETE /articles/{id}` endpoints as a part of the path. NestJS parses the `id` parameter as a string from the URL path. Then, the string is cast to a number inside your application code before being passed to the `ArticlesService`. For example, take a look at the `DELETE /articles/{id}` route handler: ```ts // src/articles/articles.controller.ts @@ -336,6 +302,7 @@ remove(@Param('id') id: string) { // id is parsed as a string return this.articlesService.remove(+id); // id is converted to number using the expression '+id' } ``` + Since `id` is defined as a string type, the Swagger API also documents this argument as a string in the generated API documentation. This is unintuitive and incorrect. ![](/nestjs-prisma-validation-7D056s1kOla1/imgs/id-string.png) @@ -353,8 +320,7 @@ import { Patch, Param, Delete, - NotFoundException, -+ ParseIntPipe, + ParseIntPipe, } from '@nestjs/common'; export class ArticlesController { @@ -362,44 +328,61 @@ export class ArticlesController { @Get(':id') @ApiOkResponse({ type: ArticleEntity }) -+ findOne(@Param('id', ParseIntPipe) id: number) { -+ return this.articlesService.findOne(id); + findOne(@Param('id', ParseIntPipe) id: number) { + return this.articlesService.findOne(id); } @Patch(':id') - @ApiCreatedResponse({ type: ArticleEntity }) + @ApiOkResponse({ type: ArticleEntity }) update( -+ @Param('id', ParseIntPipe) id: number, + @Param('id', ParseIntPipe) id: number, @Body() updateArticleDto: UpdateArticleDto, ) { -+ return this.articlesService.update(id, updateArticleDto); + return this.articlesService.update(id, updateArticleDto); } @Delete(':id') @ApiOkResponse({ type: ArticleEntity }) -+ remove(@Param('id', ParseIntPipe) id: number) { -+ return this.articlesService.remove(id); + remove(@Param('id', ParseIntPipe) id: number) { + return this.articlesService.remove(id); } } ``` -The `ParseIntPipe` will intercept the `id` parameter of string type and automatically parse it to a number before passing it to the appropriate route handler. This also has the advantage of documenting the `id` parameter correctly as a number inside Swagger. -![](/nestjs-prisma-validation-7D056s1kOla1/imgs/id-string.png) +The `ParseIntPipe` will intercept the `id` parameter of string type and automatically parse it to a number before passing it to the appropriate route handler. This also has the advantage of documenting the `id` parameter correctly as a number inside Swagger. If a client sends a value that can't be parsed to a number, like `GET /articles/abc`, the pipe rejects the request with an HTTP 400 response: -## Summary and final remarks - -Congratulations! In this tutorial, you took an existing REST API and: +```json +{ + "message": "Validation failed (numeric string is expected)", + "error": "Bad Request", + "statusCode": 400 +} +``` -- Integrated validation using the `ValidationPipe`. -- Stripped client request of unnecessary properties. -- Integrated `ParseIntPipe` to parse a `string` path variable and convert it to a `number`. +## Frequently asked questions -You might have noticed that NestJS heavily relies on decorators. This is a very intentional design choice. NestJS aims to improve code readability and modularity by heavily leveraging decorators for various kinds of [cross-cutting concerns](https://en.wikipedia.org/wiki/Cross-cutting_concern). As a result, controllers and service methods do not need to be bloated with boilerplate code for doing things like validation, caching, logging, etc. + + +No. With `whitelist: true`, the `ValidationPipe` silently strips unknown properties from the request body and processes the rest. If you want the API to reject such requests instead, add `forbidNonWhitelisted: true` alongside it; the request then fails with an HTTP 400 response like `"property extraField should not exist"`. + + +Yes. The `ValidationPipe` depends on both packages: the validation rules come from `class-validator` decorators on your DTO classes, and `class-transformer` converts the plain JSON request body into instances of those classes so the rules can be evaluated. + + +Use a transformation pipe on the parameter, for example `@Param('id', ParseIntPipe) id: number`. The pipe converts the string from the URL into a number, documents the parameter correctly in Swagger, and rejects non-numeric values with an HTTP 400 response. + + -If you want to extend the project further, the [Prisma getting started guide](https://www.prisma.io/docs/getting-started) and [Prisma Migrate docs](https://www.prisma.io/docs/orm/prisma-migrate/getting-started) are good next steps, and [Prisma Postgres](https://www.prisma.io/postgres) is an easy way to stand up the database layer for follow-on work. +## Summary and final remarks -You can find the finished code for this tutorial in the [end-validation branch](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma/tree/end-validation) of the [GitHub repository](https://github.com/prisma/blog-backend-rest-api-nestjs-prisma). Please feel free to raise an issue in the repository or submit a PR if you notice a problem. You can also reach out to me directly on [Twitter](https://twitter.com/tasinishmam). +Congratulations! In this tutorial, you took an existing REST API built with NestJS 11 and Prisma ORM 7 and: +- Integrated validation using the `ValidationPipe`. +- Stripped client requests of unnecessary properties with the `whitelist` option. +- Integrated `ParseIntPipe` to parse a `string` path variable and convert it to a `number`. +You might have noticed that NestJS heavily relies on decorators. This is a very intentional design choice. NestJS aims to improve code readability and modularity by heavily leveraging decorators for various kinds of cross-cutting concerns. As a result, controllers and service methods do not need to be bloated with boilerplate code for doing things like validation, caching, logging, etc. +In the [next part](/nestjs-prisma-error-handling-7D056s1kOop2) of this series, you will learn how to handle errors in a NestJS and Prisma application, including the database errors that validation alone can't catch. If you want to go deeper on the Prisma side first, the [Prisma getting started guide](https://www.prisma.io/docs/getting-started) and [Prisma Migrate docs](https://www.prisma.io/docs/orm/prisma-migrate/getting-started) are good next steps, and Prisma Postgres is an easy way to stand up the database layer for follow-on work. +Looking ahead: [Prisma Next](https://www.prisma.io/docs/orm) is a TypeScript-native rewrite of Prisma ORM, built for AI coding agents and currently in early access. It becomes Prisma 8 at general availability; until then, Prisma 7 stays the production choice. To try it, run `npm create prisma@next` or read the [early access docs](https://pris.ly/pn-ea).