Notification Nation: Building a Next.js Alert System in 3 Chaotic Steps
Let's face it, notifications are the hyperactive toddlers of the app world - adorably distracting and impossible to ignore. But here's the kicker: whipping up a real-time notification system is about as easy as a cat video going viral! π±
The Tech Stack (AKA Our Weapons of Mass Notification)
Before we start spamming... I mean, notifying our users, let's check out our techstack:
- React: The OG of declarative UI libraries π
- Next.js: React's overachieving cousin
- Prisma: The database whisperer
Now, let's get our hands dirty with some code! (Don't worry, we have sanitizer. π§Ό)
Step 1: Database Shenanigans
First up, we need to design our database schema. It's like architecting a house, but instead of rooms, we're building tables. Here's our masterpiece:
model Notification {
notificationId String @id @default(cuid())
notificationType NotificationType
recipientUserId String
senderUserId String
relatedPostId String?
creationTimestamp DateTime @default(now())
readTimestamp DateTime?
recipient User @relation("ReceivedNotifications", fields: [recipientUserId], references: [id])
sender User @relation("SentNotifications", fields: [senderUserId], references: [id])
relatedPost Post? @relation(fields: [relatedPostId], references: [id])
}
enum NotificationType {
FOLLOW
REPLY
LIKE
}
This schema is like a recipe for the world's most exciting database casserole. Bon appΓ©tit! π²
Prisma Transactions: The Unsung Heroes of Database Integrity
Hold onto your keyboards, folks, because we're about to talk about the coolest thing since sliced bread in database land: Prisma transactions! ππ½
Picture this: you're creating a notification, updating a user's notification count, and maybe even sending an email all at once. Sounds like a recipe for a database disaster, right? Enter Prisma transactions, swooping in like a superhero to save your data day! π¦ΈββοΈ
Step 2: API Route Magic
Next up, we're creating an API route faster than you can say "server-side rendering".
Check out this beauty:
//API route for notifications page.
// @/app/api/notifications/route.ts
export async function GET(req: NextRequest) {
try {
// Extract cursor from query params for pagination
const cursor = req.nextUrl.searchParams.get('cursor') || undefined;
const pageSize = 10;
// Authenticate the user (because we're not running a notification free-for-all here)
const { userId } = await auth();
if (!userId) {
return Response.json(
{ error: 'Nice try, but no notifications for you!' },
{ status: 401 }
);
}
// Fetch notifications using Prisma (it's like speed dating for your database)
const notifications = await db.notification.findMany({
where: { recipientUserId: userId },
include: {
sender: {
select: {
username: true,
fullName: true,
profileImage: true
},
},
recipient: {
select: {
username: true,
fullName: true,
profileImage: true,
},
},
},
orderBy: { creationTimestamp: 'desc' },// Newest gossip first
take: pageSize + 1,// Fetch one extra to see if there's more drama
cursor: cursor ? { notificationId: cursor } : undefined
});
// Determine the next cursor (it's like breadcrumbs, but for data)
const nextCursor =
notifications.length > pageSize
? notifications[pageSize]?.id ?? null
: null;
// Prepare the response data (wrapped up with a bow)
const data: NotificationsPage = {
notifications: notifications.slice(0, pageSize),
nextCursor
};
return Response.json(data);
} catch (error) {
console.error(error);
return Response.json(
{ error: 'Oops! Our hamsters stopped running.' },
{ status: 500 }
);
}
}
This route is handling pagination like a pro juggler at a circus. πͺ We're using Prisma's findMany method, which is basically a database speed dating service.
Just for a bit of extra flair, here is how we could fetch the unread notifications via an API route:
export async function GET() {
try {
const { userId } = await auth();
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const unreadCount = await prisma.notification.count({
where: {
recipientUserId: userId,
readTimestamp: null,
},
});
const data: NotificationCountInfo = {
unreadCount,
};
return Response.json(data);
} catch (error) {
console.error(error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}
Step 3: Full-Stack Notification Fiesta π
Alright, notification ninjas, its time to witness the ultimate fusion of frontend finesse and backend brilliance! Were not just pushing pixels anymore β were orchestrating a full-stack symphony of likes, notifications, and real-time updates. Grab your conductors baton, because this is where the magic happens! πͺπ΅
import { useInfiniteQuery, useQueryClient } from 'react-query';
import ky from 'ky';
export default function NotificationsPage() {
const queryClient = useQueryClient();
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['notifications'],
queryFn: ({ pageParam }) =>
// I use KY,but a simple fetch method would work too!
ky
.get('/api/notifications', {
pageParam ? { searchParams: { cursor: pageParam } } : {},
})
.json(<NotificationData>),
initialPageParam: null as string | null,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
return (
<div>
{data?.pages.map((page) =>
page.notifications.map((notification) => (
<NotificationItem
key={notification.notificationId}
notification={notification}
/>
))
)}
</div>
);
}
Here is an example Like server action that uses Prisma transactions:
// Meanwhile, in a galaxy far, far away (aka your server)...
export async function likePost(postId: string) {
'use server';
const prisma = new PrismaClient();
try {
await prisma.$transaction(async (tx) => {
await tx.like.create({
data: {
postId,
userId: currentUserId
}
});
const post = await tx.post.findUnique({
where: { id: postId },
select: { authorId: true }
});
if (!post) throw new Error('Post not found. Did it go on vacation? ποΈ');
await tx.notification.create({
data: {
notificationType: 'LIKE',
recipientUserId: post.authorId,
senderUserId: currentUserId,
relatedPostId: postId
}
});
await tx.post.update({
where: { id: postId },
data: { likeCount: { increment: 1 } }
});
});
return { success: true };
} catch (error) {
console.error('Like operation failed. Did the server eat your homework?', error);
return { success: false, error: 'Failed to add like. Time to debug! π' };
}
}
How to handle unread notifications with React Query:
const { data } = useQuery({
queryKey: ["unread-notification"],
queryFn: () =>
kyInstance
.get("/api/notifications/unread")
.json<NotificationType>(),
refetchInterval: 60 \* 1000,
});
The Results

Wrapping It Up
And there you have it, folks! We've just built a notification system so slick, it could slide into your DMs wearing socks on a hardwood floor. π§¦