March 23, 2023

Building a REST API with NestJS and Prisma: Handling Relational Data

8 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.

Building a REST API with NestJS and Prisma: Handling Relational Data

Table Of Contents

Introduction

In the first chapter 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, you learned how to do input validation and transformation.

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).
  2. Next, you will implement the API routes for the User endpoints to perform CRUD (create, read, update and delete) operations on User records.
  3. 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.

Development environment

To follow along with this tutorial, you will be expected to:

  • ... have Node.js installed.
  • ... have Docker and Docker Compose 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 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.

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

The starting point for this tutorial is the ending of chapter two of this series. It contains a rudimentary REST API built with NestJS.

The starting point for this tutorial is available in the end-validation branch of the GitHub repository. To get started, clone the repository and checkout the end-validation branch:

Now, perform the following actions to get started:

  1. Navigate to the cloned directory:
  1. Install dependencies:
  1. Start the PostgreSQL database with Docker:
  1. Apply database migrations:
  1. Start the project:

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/.

Project structure and files

The repository you cloned should have the following structure:

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

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 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.

Note: For more information about these components, go through chapter one of this tutorial series.

Add a User model to the database

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:

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:

If the migration runs successfully, you should see the following output:

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:

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:

If the seed script runs successfully, you should see the following output:

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.

Inside ArticleEntity add a new authorId field:

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 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

To generate a new REST resource for users run the following command:

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
  2. What transport layer do you use? REST API
  3. Would you like to generate CRUD entry points? Yes

You should now find a new users module in the src/users directory with all the boilerplate for your REST endpoints.

Inside the src/users/users.controller.ts file, you will see the definition of different routes (also called route handlers). The business logic for handling each request is encapsulated in the src/users/users.service.ts file.

If you open the Swagger generated API page, you should see something like this:

Auto-generated "users" endpoints

Add PrismaClient to the Users module

To access PrismaClient inside the Users module, you must add the PrismaModule as an import. Add the following imports to 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:

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:

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.

A DTO (Data Transfer Object) is an object that defines how the data will be sent over the network. You will need to implement the CreateUserDto and UpdateUserDto classes to define the data that will be sent to the API when creating and updating a user, respectively. Define the CreateUserDto class inside the create-user.dto.ts file as follows:

@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 of this series.

The definition of UpdateUserDto is automatically inferred from the CreateUserDto definition, so it does not need to be defined explicitly.

Define the UsersService class

The UsersService is responsible for modifying and fetching data from the database using Prisma Client and providing it to the UsersController. You will implement the create(), findAll(), findOne(), update() and remove() methods in this class.

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.

The controller consists of different route handlers. You will implement five route handlers in this class that correspond to five endpoints:

  • create() - POST /users
  • findAll() - GET /users
  • findOne() - GET /users/:id
  • update() - PATCH /users/:id
  • remove() - DELETE /users/:id

Update the implementation of these route handlers in users.controller.ts as follows:

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 should look like this

Updated swagger page

Feel free to test the different endpoints to verify they behave as expected.

Exclude password field from the response body

While the users API works as expected, it has a major security flaw. The password field is returned in the response body of the different endpoints.

GET /users/:id reveals password

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 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

Interceptors in NestJS allow you to hook into the request-response cycle and allow you to execute extra logic before and after the route handler is executed. In this case, you will use it to remove the password field from the response body.

NestJS has a built-in ClassSerializerInterceptor that can be used to transform objects. You will use this interceptor to remove the password field from the response object.

First, enable ClassSerializerInterceptor globally by updating main.ts:

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.

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:

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.

First, you need to create a constructor that will instantiate a UserEntity object.

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<UserEntity>. 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:

Now, the password should be omitted from the response object.

GET /users/:id does not reveal password

Returning the author along with an article

In chapter one you implemented the GET /articles/:id endpoint for retrieving a single article. Currently, this endpoint does not return the author of an article, only the authorId. In order to fetch the author you have to make an additional request to the GET /users/:id endpoint. This is not ideal if you need both the article and its author because you need to make two API requests. You can improve this by returning the author along with the Article object.

The data access logic is implemented inside the ArticlesService. Update the findOne() method to return the author along with the Article object:

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

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.

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.

Now update the ArticlesController to return instances of ArticleEntity objects:

Now, GET /articles/:id returns the author object without the password field:

GET /articles/:id does not reveal password

Summary and final remarks

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 branch of the GitHub repository. 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.

Don’t miss the next post!

Sign up for the Prisma Newsletter