By implementing the post like feature with Prisma and GraphQL, you can learn how to design efficient database models, handle simple relationships (such as the one-to-many relationship between posts and likes), and manage data queries and mutations in GraphQL.
Add a Like
model to prisma.schema
and sync the database with npx prisma migrate dev --name add_like
model Like {
id String @id @default(cuid())
postId String
post Post @relation(fields: [postId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User @relation(fields: [createdById], references: [id])
createdById String
@@unique([createdById, postId]) // Ensure each user can only like a post once
}
Add likePost
and unlikePost
APIs:
// graphql/types/Like.ts
import { builder } from '../builder'
builder.prismaObject('Like', {
fields: (t) => ({
id: t.exposeID('id'),
postId: t.exposeID('postId'),
post: t.relation('post'),
createdAt: t.expose('createdAt', { type: 'DateTime' }),
updatedAt: t.expose('updatedAt', { type: 'DateTime' }),
createdById: t.exposeID('createdById'),
createdBy: t.relation('createdBy'),
}),
})
builder.mutationType({
fields: (t) => ({
likePost: t.field({
type: 'Like' as any,
args: {
postId: t.arg.id(),
userId: t.arg.id(),
},
resolve: async (_root: any, args: any, ctx: any) => {
const { postId, userId } = args
const like = await ctx.prisma.like.create({
data: {
postId,
createdById: userId,
},
})
return like
},
}),
unlikePost: t.field({
type: 'Like' as any,
args: {
postId: t.arg.id(),
userId: t.arg.id(),
},
resolve: async (_root: any, args: any, ctx: any) => {
const { postId, userId } = args
const like = await ctx.prisma.like.delete({
where: {
createdById_postId: {
createdById: userId,
postId,
},
},
})
return like
},
}),
}),
})
likeCount
: count the number of likes for a postisLikedByUser
: check if the current user has liked the post// graphql/types/Post.ts
import prisma from '@/lib/prisma'
import { builder } from '../builder'
builder.prismaObject('Post', {
fields: (t: any) => ({
//...
likes: t.relation('likes'),
likeCount: t.int({
resolve: async (parent: any, _args: any, ctx: any) => {
const likeCount = await ctx.prisma.like.count({
where: {
postId: parent.id,
},
})
return likeCount
},
}),
isLikedByUser: t.boolean({
args: {
userId: t.arg.id(),
},
resolve: async (parent: any, _args: any, ctx: any) => {
const { userId } = _args
if (!userId) {
return false
}
const like = await ctx.prisma.like.findUnique({
where: {
createdById_postId: {
createdById: userId,
postId: parent.id,
},
},
})
return like !== null
},
}),
}),
})
// Define mutation type for deleting a post
builder.mutationType({
fields: (t: any) => ({
deletePost: t.field({
type: 'Post' as any,
args: {
id: t.arg.id(),
},
resolve: async (_root: any, args: any, ctx: any) => {
const { id } = args
// Delete Like records related to the Post
await ctx.prisma.like.deleteMany({
where: { postId: id },
})
// Then delete the Post
await ctx.prisma.post.delete({
where: { id },
})
return true
},
}),
}),
} as any)
// LikeAction.tsx
'use client'
import { Button } from '@/components/ui/button'
import { gql, useMutation } from '@apollo/client'
import { Heart } from 'lucide-react'
import { useLocale } from 'next-intl'
import { useState } from 'react'
import { toast } from 'sonner'
type Props = {
post: any
currentUserId?: string
}
const LikeAction = ({ post, currentUserId }: Props) => {
const locale = useLocale()
const [isLiked, setIsLiked] = useState<boolean>(post.isLikedByUser)
const [likeCount, setLikeCount] = useState<number>(post.likeCount)
const [likePost, { loading: likingPost }] = useMutation(
gql`
mutation LikePost($postId: ID!, $userId: ID!) {
likePost(postId: $postId, userId: $userId) {
id
}
}
`,
{
onCompleted: (data) => {
setIsLiked(true)
setLikeCount((prev) => prev + 1)
},
},
)
const [unlikePost, { loading: unlikingPost }] = useMutation(
gql`
mutation UnlikePost($postId: ID!, $userId: ID!) {
unlikePost(postId: $postId, userId: $userId) {
id
}
}
`,
{
onCompleted: (data) => {
setIsLiked(false)
setLikeCount((prev) => prev - 1)
},
},
)
const handleLikeToggle = () => {
if (!currentUserId) {
toast.info('Please login to like this post')
}
if (isLiked) {
unlikePost({
variables: { postId: post.id, userId: currentUserId },
})
} else {
likePost({
variables: { postId: post.id, userId: currentUserId },
})
}
}
return (
<Button
variant="ghost"
size="sm"
onClick={handleLikeToggle}
disabled={likingPost || unlikingPost}
className={`flex cursor-pointer items-center gap-1 ${isLiked ? 'text-red-500' : 'text-gray-500'}`}
>
<Heart className={`h-4 w-4 ${isLiked ? 'fill-current' : ''}`} />
<span>{likeCount}</span>
</Button>
)
}
export default LikeAction