← Back to Blog

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

Tasin Ishmam
Tasin Ishmam
March 23, 2023
Updated July 8, 2026
10 min read

Prisma ORM models relations declaratively: you define a relation field in the schema, and queries opt into loading related records with include. This tutorial is the fourth part of a five-part series on building a REST API with NestJS and Prisma ORM. In it, you will model a one-to-many relation between users and articles, build CRUD endpoints for users, keep the password field out of API responses, and return an article's author along with the article.

Updated (July 2026): This tutorial has been fully revised for Prisma ORM 7 and NestJS 11, and continues from the updated third part 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.

Introduction

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

Development environment

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

  • ... have Node.js (v18 or higher) installed.
  • ... have a PostgreSQL database. The easiest option is Prisma Postgres; you can also run Postgres locally with Docker or a native install.
  • ... have the Prisma VSCode Extension 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.

Set up the project

The starting point for this tutorial is the ending of part three 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.

Before continuing, make sure the project runs:

  1. Start the development server:
npm run start:dev
  1. Confirm the API documentation is available at 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

Your project should have the following structure:

median
  ├── node_modules
  ├── generated
  │   └── prisma
  ├── prisma
  │   ├── migrations
  │   ├── schema.prisma
  │   └── seed.ts
  ├── src
  │   ├── app.module.ts
  │   ├── main.ts
  │   ├── articles
  │   ├── prisma
  │   └── prisma-client-exception
  ├── test
  │   ├── app.e2e-spec.ts
  │   └── jest-e2e.json
  ├── README.md
  ├── .env
  ├── 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 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 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 PrismaService, your interface to the database.
    • The articles module defines the endpoints for the /articles route and accompanying business logic.
    • The prisma-client-exception directory contains the exception filter you built in part three.
  • 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 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 Article model gets two new fields, author and authorId, and the schema gets a new User model:

// prisma/schema.prisma

model Article {
  id          Int      @id @default(autoincrement())
  title       String   @unique
  description String?
  body        String
  published   Boolean  @default(false)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  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[]
}

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:

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/
  └─ 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:

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 the main function as follows:

// 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',
    },
  });

  // create three dummy articles
  const post1 = await prisma.article.upsert({
    where: { title: 'Prisma Adds Support for MongoDB' },
    update: {
      authorId: user1.id,
    },
    create: {
      title: 'Prisma Adds Support for MongoDB',
      body: 'Support for MongoDB has been one of the most requested features since the initial release of...',
      description:
        "We are excited to share that today's Prisma ORM release adds stable support for MongoDB!",
      published: false,
      authorId: user1.id,
    },
  });

  const post2 = await prisma.article.upsert({
    where: { title: "What's new in Prisma? (Q1/22)" },
    update: {
      authorId: user2.id,
    },
    create: {
      title: "What's new in Prisma? (Q1/22)",
      body: 'Our engineers have been working hard, issuing new releases with many improvements...',
      description:
        'Learn about everything in the Prisma ecosystem and community from January to March 2022.',
      published: true,
      authorId: user2.id,
    },
  });

  const post3 = await prisma.article.upsert({
    where: { title: 'Prisma Client Just Became a Lot More Flexible' },
    update: {},
    create: {
      title: 'Prisma Client Just Became a Lot More Flexible',
      body: 'Prisma Client extensions provide a powerful new way to add functionality to Prisma in a type-safe manner...',
      description:
        'This article will explore various ways you can use Prisma Client extensions to add custom functionality to Prisma Client..',
      published: true,
    },
  });

  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:

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

// src/articles/entities/article.entity.ts

import { Article } from '../../../generated/prisma/client';
import { ApiProperty } from '@nestjs/swagger';

export class ArticleEntity implements Article {
  @ApiProperty()
  id: number;

  @ApiProperty()
  title: string;

  @ApiProperty({ required: false, nullable: true })
  description: string | null;

  @ApiProperty()
  body: string;

  @ApiProperty()
  published: boolean;

  @ApiProperty()
  createdAt: Date;

  @ApiProperty()
  updatedAt: Date;

  @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 the 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:

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

// src/users/users.module.ts

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { PrismaModule } from '../prisma/prisma.module';

@Module({
  controllers: [UsersController],
  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:

// 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 '../prisma/prisma.service';

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

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

// src/users/entities/user.entity.ts

import { ApiProperty } from '@nestjs/swagger';
import { User } from '../../../generated/prisma/client';

export class UserEntity implements User {
  @ApiProperty()
  id: number;

  @ApiProperty()
  createdAt: Date;

  @ApiProperty()
  updatedAt: Date;

  @ApiProperty({ required: false, nullable: true })
  name: string | null;

  @ApiProperty()
  email: string;

  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.

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:

// src/users/dto/create-user.dto.ts

import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  @ApiProperty()
  name: string;

  @IsString()
  @IsNotEmpty()
  @ApiProperty()
  email: string;

  @IsString()
  @IsNotEmpty()
  @MinLength(6)
  @ApiProperty()
  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 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.

// 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 '../prisma/prisma.service';

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

  create(createUserDto: CreateUserDto) {
    return this.prisma.user.create({ data: createUserDto });
  }

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

  findOne(id: number) {
    return this.prisma.user.findUnique({ where: { id } });
  }

  update(id: number, updateUserDto: UpdateUserDto) {
    return this.prisma.user.update({ where: { id }, data: updateUserDto });
  }

  remove(id: number) {
    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.

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:

// src/users/users.controller.ts

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  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';

@Controller('users')
@ApiTags('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  @ApiCreatedResponse({ type: UserEntity })
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Get()
  @ApiOkResponse({ type: UserEntity, isArray: true })
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':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')
  @ApiOkResponse({ type: UserEntity })
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUserDto,
  ) {
    return this.usersService.update(id, updateUserDto);
  }

  @Delete(':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 findOne handler additionally throws a NotFoundException when no user exists with the given id, following the error handling pattern from part three; this also satisfies strictNullChecks, since findOne in the service can return null.

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:

// src/main.ts

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

  app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
  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);

  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.

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:

// src/users/entities/user.entity.ts

import { ApiProperty } from '@nestjs/swagger';
import { User } from '../../../generated/prisma/client';
import { Exclude } from 'class-transformer';

export class UserEntity implements User {
  @ApiProperty()
  id: number;

  @ApiProperty()
  createdAt: Date;

  @ApiProperty()
  updatedAt: Date;

  @ApiProperty({ required: false, nullable: true })
  name: string | null;

  @ApiProperty()
  email: string;

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

// src/users/entities/user.entity.ts

import { ApiProperty } from '@nestjs/swagger';
import { User } from '../../../generated/prisma/client';
import { Exclude } from 'class-transformer';

export class UserEntity implements User {
  constructor(partial: Partial<UserEntity>) {
    Object.assign(this, partial);
  }

  @ApiProperty()
  id: number;

  @ApiProperty()
  createdAt: Date;

  @ApiProperty()
  updatedAt: Date;

  @ApiProperty({ required: false, nullable: true })
  name: string | null;

  @ApiProperty()
  email: string;

  @Exclude()
  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<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:

// src/users/users.controller.ts

@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()
  @ApiOkResponse({ type: UserEntity, isArray: true })
  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) {
    const user = await this.usersService.findOne(id);
    if (!user) {
      throw new NotFoundException(`User with ${id} does not exist.`);
    }
    return new UserEntity(user);
  }

  @Patch(':id')
  @ApiOkResponse({ type: UserEntity })
  async update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: 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));
  }
}

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:

// src/articles/articles.service.ts

  findOne(id: number) {
    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

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.

// src/articles/entities/article.entity.ts

import { Article } from '../../../generated/prisma/client';
import { ApiProperty } from '@nestjs/swagger';
import { UserEntity } from '../../users/entities/user.entity';

export class ArticleEntity implements Article {
  @ApiProperty()
  id: number;

  @ApiProperty()
  title: string;

  @ApiProperty({ required: false, nullable: true })
  description: string | null;

  @ApiProperty()
  body: string;

  @ApiProperty()
  published: boolean;

  @ApiProperty()
  createdAt: Date;

  @ApiProperty()
  updatedAt: Date;

  @ApiProperty({ required: false, nullable: true })
  authorId: number | null;

  @ApiProperty({ required: false, type: UserEntity })
  author?: UserEntity | null;

  constructor({ author, ...data }: Partial<ArticleEntity>) {
    Object.assign(this, data);

    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:

// src/articles/articles.controller.ts

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  ParseIntPipe,
  NotFoundException,
} from '@nestjs/common';
import { ArticlesService } from './articles.service';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';
import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { ArticleEntity } from './entities/article.entity';

@Controller('articles')
@ApiTags('articles')
export class ArticlesController {
  constructor(private readonly articlesService: ArticlesService) {}

  @Post()
  @ApiCreatedResponse({ type: ArticleEntity })
  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));
  }

  @Get('drafts')
  @ApiOkResponse({ type: ArticleEntity, isArray: true })
  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) {
    const article = await this.articlesService.findOne(id);
    if (!article) {
      throw new NotFoundException(`Article with ${id} does not exist.`);
    }
    return new ArticleEntity(article);
  }

  @Patch(':id')
  @ApiOkResponse({ type: ArticleEntity })
  async update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateArticleDto: 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));
  }
}

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

GET /articles/:id does not reveal password

Frequently asked questions

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 the next part 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 cover one-to-many and other relation types in depth, and Prisma Postgres is an easy way to stand up the database layer for follow-on work.

Looking ahead: Prisma Next 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.

Build your next app with Prisma

Start free. Scale when you’re ready.

Try Prisma
Share this article