Scope of discussion:
- Create CRUD for
posts
endpoints - Protect
edit
anddelete
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:
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:
// 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
:
// 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
:
// 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:
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:
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
-
GET /posts
-
GET /posts/:id
-
PATCH /posts/:id
-
DELETE /posts/: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)