DEV Community

Alfi Samudro Mulyo
Alfi Samudro Mulyo

Posted on • Edited on

Build Complete REST API Feature with Nest JS (Using Prisma and Postgresql) from Scratch - Beginner-friendly - PART 3

Scope of discussion:

  1. Create CRUD for posts endpoints
  2. Protect edit and delete routes access from non-author

In the second part, we've handled route protection by using auth.guard. Some of the endpoints are public (like login and register). By default, the routes will be defined as private that need access_token.

Also, we protect a user from updating/deleting other user's data by using is-mine.guard

So far, we've completed the users routes with basic CRUD, Validation, Authentication, and Route protection. Now let's move on to the Post model.

Post model will have the same basic CRUD, validation, and route protection as we did in User model. Here are the Post endpoints:

  • POST /posts: Create a new post,
  • GET /posts: Get all posts,
  • GET /posts/:id: Get post by ID,
  • PATCH /posts/:id: Update post by ID,
  • DELETE /posts/:id: Delete post by ID.

We'll use the same project structure for building Post. Let's recall what are the files inside src/modules/users folder:

folder tree

Let's start with creating three main files:

  • src/modules/posts/posts.service.ts
  • src/modules/posts/posts.module.ts
  • src/modules/posts/posts.controller.ts

and dto files:

  • src/modules/posts/dtos/create-post.dto.ts ```typescript

// src/modules/posts/dtos/create-post.dto.ts

import { IsNotEmpty } from 'class-validator';

export class CreatePostDto {
@IsNotEmpty()
title: "string;"

@IsNotEmpty()
content: string;

@IsNotEmpty()
published: boolean = false;

authorId: number;
}

- `src/modules/posts/dtos/update-post.dto.ts`
```typescript


// src/modules/posts/dtos/update-post.dto.ts

import { PartialType } from '@nestjs/mapped-types';
import { CreatePostDto } from './create-post.dto';

export class UpdatePostDto extends PartialType(CreatePostDto) {}


Enter fullscreen mode Exit fullscreen mode

We'll start building Post endpoints from the controller. Here is what the src/modules/posts/posts.controller.ts will look like:



// src/modules/posts/posts.controller.ts

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  ParseIntPipe,
  Patch,
  Post,
  Request,
  UseGuards,
} from '@nestjs/common';
import { CreatePostDto } from './dtos/create-post.dto';
import { UpdatePostDto } from './dtos/update-post.dto';
import { PostsService } from './posts.service';
import { Post as CPost } from '@prisma/client';
import { Public } from 'src/common/decorators/public.decorator';
import { IsMineGuard } from 'src/common/guards/is-mine.guard';
import { ExpressRequestWithUser } from '../users/interfaces/express-request-with-user.interface';

@Controller('posts')
export class PostsController {
  // inject posts service
  constructor(private readonly postsService: PostsService) {}

  @Post()
  async createPost(
    @Body() createPostDto: CreatePostDto,
    @Request() req: ExpressRequestWithUser,
  ): Promise<CPost> {
    // 💡 See this. set authorId to current user's id
    createPostDto.authorId = req.user.sub;
    return this.postsService.createPost(createPostDto);
  }

  @Public()
  @Get()
  getAllPosts(): Promise<CPost[]> {
    return this.postsService.getAllPosts();
  }

  @Public()
  @Get(':id')
  getPostById(@Param('id', ParseIntPipe) id: number): Promise<CPost> {
    return this.postsService.getPostById(id);
  }

  @Patch(':id')
  @UseGuards(IsMineGuard) // <--- 💡 Prevent user from updating other user's posts
  async updatePost(
    @Param('id', ParseIntPipe) id: number,
    @Body() updatePostDto: UpdatePostDto,
  ): Promise<CPost> {
    return this.postsService.updatePost(+id, updatePostDto);
  }

  @Delete(':id')
  @UseGuards(IsMineGuard) // <--- 💡 Prevent user from deleting other user's posts
  async deletePost(@Param('id', ParseIntPipe) id: number): Promise<string> {
    return this.postsService.deletePost(+id);
  }
}


Enter fullscreen mode Exit fullscreen mode

Then, let's create posts.service.ts:



// src/modules/posts/posts.service.ts

import {
  ConflictException,
  HttpException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { Post } from '@prisma/client';
import { PrismaService } from 'src/core/services/prisma.service';
import { CreatePostDto } from './dtos/create-post.dto';
import { UpdatePostDto } from './dtos/update-post.dto';

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

  async createPost(createPostDto: CreatePostDto): Promise<Post> {
    try {
      // create new post using prisma client
      const newPost = await this.prisma.post.create({
        data: {
          ...createPostDto,
        },
      });

      return newPost;
    } catch (error) {
      // check if email already registered and throw error
      if (error.code === 'P2002') {
        throw new ConflictException('Email already registered');
      }

      if (error.code === 'P2003') {
        throw new NotFoundException('Author not found');
      }

      // throw error if any
      throw new HttpException(error, 500);
    }
  }

  async getAllPosts(): Promise<Post[]> {
    const posts = await this.prisma.post.findMany();

    return posts;
  }

  async getPostById(id: number): Promise<Post> {
    try {
      // find post by id. If not found, throw error
      const post = await this.prisma.post.findUniqueOrThrow({
        where: { id },
      });

      return post;
    } catch (error) {
      // check if post not found and throw error
      if (error.code === 'P2025') {
        throw new NotFoundException(`Post with id ${id} not found`);
      }

      // throw error if any
      throw new HttpException(error, 500);
    }
  }

  async updatePost(id: number, updatePostDto: UpdatePostDto): Promise<Post> {
    try {
      // find post by id. If not found, throw error
      await this.prisma.post.findUniqueOrThrow({
        where: { id },
      });

      // update post using prisma client
      const updatedPost = await this.prisma.post.update({
        where: { id },
        data: {
          ...updatePostDto,
        },
      });

      return updatedPost;
    } catch (error) {
      // check if post not found and throw error
      if (error.code === 'P2025') {
        throw new NotFoundException(`Post with id ${id} not found`);
      }

      // check if email already registered and throw error
      if (error.code === 'P2002') {
        throw new ConflictException('Email already registered');
      }

      // throw error if any
      throw new HttpException(error, 500);
    }
  }

  async deletePost(id: number): Promise<string> {
    try {
      // find post by id. If not found, throw error
      const post = await this.prisma.post.findUniqueOrThrow({
        where: { id },
      });

      // delete post using prisma client
      await this.prisma.post.delete({
        where: { id },
      });

      return `Post with id ${post.id} deleted`;
    } catch (error) {
      // check if post not found and throw error
      if (error.code === 'P2025') {
        throw new NotFoundException(`Post with id ${id} not found`);
      }

      // throw error if any
      throw new HttpException(error, 500);
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

Then, let's create posts.module.ts:



// src/modules/posts/posts.module.ts

import { Module } from '@nestjs/common';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';

@Module({
  imports: [],
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}


Enter fullscreen mode Exit fullscreen mode

Now we've completed the three main files:

  • src/modules/posts/posts.module.ts ✅
  • src/modules/posts/posts.controller.ts ✅
  • src/modules/posts/posts.service.ts ✅

We need to update is-mine.guard.ts to handle Post. Let's update the canActivate function:



async canActivate(context: ExecutionContext): Promise<boolean> {
  const request = context.switchToHttp().getRequest();

  // 💡 We can access the user payload from the request object
  // because we assigned it in the AuthGuard

  // 💡 Get instance of the route by splitting the path, e.g. /posts/1
  const route = request.route.path.split('/')[1];
  const paramId = isNaN(parseInt(request.params.id))
    ? 0
    : parseInt(request.params.id);

  switch (route) {
    // 💡 Check if the post belongs to the user
    case 'posts':
      const post = await this.prismaService.post.findFirst({
        where: {
          id: paramId,
          authorId: request.user.sub,
        },
      });

      return paramId === post?.id;
    default:
      // 💡 Check if the user manages its own profile
      return paramId === request.user.sub;
  }
}


Enter fullscreen mode Exit fullscreen mode

Last, we need to import PostsModule to AppModule. Open up app.module.ts file and modify:



import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './modules/users/users.module';
import { CoreModule } from './core/core.module';
import { JwtModule } from '@nestjs/jwt';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './common/guards/auth.guard';
import { PostsModule } from './modules/posts/posts.module';

@Module({
  imports: [
    UsersModule,
    PostsModule,
    CoreModule,
    // add jwt module
    JwtModule.register({
      global: true,
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: '12h' },
    }),
  ],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
  ],
})
export class AppModule {}


Enter fullscreen mode Exit fullscreen mode

We've completed the Post routes:

  • POST /posts Create post
  • GET /posts Get all posts
  • GET /posts/:id Get post by ID
  • PATCH /posts/:id Update post by ID
  • DELETE /posts/:id Delete post by ID

The full code of part 3 can be accessed here: https://github.com/alfism1/nestjs-api/tree/part-three

Moving on to part 4: https://dev.to/alfism1/build-complete-rest-api-feature-with-nest-js-using-prisma-and-postgresql-from-scratch-beginner-friendly-part-4-oad

Top comments (0)