Build Complete REST API Feature with Nest JS (Using Prisma and Postgresql) from Scratch - Beginner-friendly - PART 3
Tags: nestjs, typescript, api, backend
Published: 2023-10-28 01:12:59

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/18u5r0fjf6sfqv85hpe3.png)

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) {}
```


We'll start building `Post` endpoints from the controller. Here is what the `src/modules/posts/posts.controller.ts` will look like:
```typescript
// 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);
  }
}
```

Then, let's create `posts.service.ts`:
```typescript
// 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);
    }
  }
}
```

Then, let's create `posts.module.ts`:
```typescript
// 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 {}
```

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:
```typescript
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;
  }
}
```

Last, we need to import `PostsModule` to `AppModule`. Open up `app.module.ts` file and modify:
```typescript
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 {}
```

We've completed the `Post` routes:
- `POST /posts`
![Create post](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6938euotxin8pgmvlmx1.png)
- `GET /posts`
![Get all posts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jhscrwjujaethq0j9ap2.png)
- `GET /posts/:id`
![Get post by ID](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u5ycvw03mf5cx1g9bcw1.png)
- `PATCH /posts/:id`
![Update post by ID](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mbg17vynujcbmilsjr0i.png)
- `DELETE /posts/:id`
![Delete post by ID](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oim6fn4pz35d86j4qr7h.png)

---

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