by shahin
September 3, 2024

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:

Untitled-1
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:

Untitled-1
//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:

Untitled-1
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! πŸͺ„πŸŽ΅

Untitled-1
 
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:

Untitled-1
// 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:

Untitled-1
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. 🧦