Next.js

Email & Notifications

23 min Lesson 30 of 40

Email & Notifications

Email and notification systems are critical for user engagement and communication. This lesson covers sending emails, implementing notification systems, using React Email for beautiful templates, and managing background jobs.

Email Sending Options

Several approaches for sending emails in Next.js:

/* 1. Nodemailer - Self-hosted SMTP */ Pros: Full control, no API limits Cons: Configuration complexity, deliverability concerns /* 2. SendGrid - Transactional email service */ Pros: Reliable delivery, analytics, templates Cons: Pricing, API rate limits /* 3. Resend - Modern email API */ Pros: Developer-friendly, React Email support Cons: Newer service, smaller ecosystem /* 4. AWS SES - Amazon email service */ Pros: Cost-effective, high volume Cons: AWS complexity, verification requirements

Nodemailer Setup

Send emails using SMTP with Nodemailer:

Install and Configure Nodemailer

# Install Nodemailer npm install nodemailer # Environment variables SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_USER=your-email@gmail.com SMTP_PASSWORD=your-app-password EMAIL_FROM=noreply@yourapp.com

Email Service Utility

// lib/email.ts import nodemailer from 'nodemailer'; const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: Number(process.env.SMTP_PORT), secure: false, // true for 465, false for other ports auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASSWORD, }, }); export async function sendEmail({ to, subject, html, text, }: { to: string; subject: string; html?: string; text?: string; }) { try { const info = await transporter.sendMail({ from: process.env.EMAIL_FROM, to, subject, text, html, }); console.log('Email sent:', info.messageId); return { success: true, messageId: info.messageId }; } catch (error) { console.error('Email error:', error); throw error; } }

Send Email API Route

// app/api/send-email/route.ts import { NextRequest } from 'next/server'; import { sendEmail } from '@/lib/email'; export async function POST(request: NextRequest) { try { const { to, subject, message } = await request.json(); await sendEmail({ to, subject, text: message, html: `<p>${message}</p>`, }); return Response.json({ success: true }); } catch (error) { return Response.json( { error: 'Failed to send email' }, { status: 500 } ); } }

Resend Integration

Resend provides a modern, developer-friendly email API:

Setup Resend

# Install Resend SDK npm install resend # Environment variable RESEND_API_KEY=re_your_api_key

Resend Email Service

// lib/resend.ts import { Resend } from 'resend'; const resend = new Resend(process.env.RESEND_API_KEY); export async function sendEmailWithResend({ to, subject, html, from = 'onboarding@resend.dev', }: { to: string; subject: string; html: string; from?: string; }) { try { const { data, error } = await resend.emails.send({ from, to, subject, html, }); if (error) { throw error; } return { success: true, id: data.id }; } catch (error) { console.error('Resend error:', error); throw error; } }

React Email for Beautiful Templates

React Email lets you build responsive email templates with React:

Install React Email

# Install React Email npm install react-email @react-email/components # Add email dev script to package.json { "scripts": { "email:dev": "email dev" } } # Preview emails in browser npm run email:dev

Create Email Template

// emails/WelcomeEmail.tsx import { Body, Button, Container, Head, Heading, Html, Link, Preview, Section, Text, } from '@react-email/components'; interface WelcomeEmailProps { username: string; loginUrl: string; } export default function WelcomeEmail({ username, loginUrl, }: WelcomeEmailProps) { return ( <Html> <Head /> <Preview>Welcome to our platform!</Preview> <Body style={main}> <Container style={container}> <Heading style={h1}>Welcome, {username}!</Heading> <Text style={text}> Thanks for signing up. We're excited to have you on board! </Text> <Section style={buttonContainer}> <Button style={button} href={loginUrl}> Get Started </Button> </Section> <Text style={text}> If you have any questions, reply to this email. </Text> </Container> </Body> </Html> ); } const main = { backgroundColor: '#f6f9fc', fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif', }; const container = { backgroundColor: '#ffffff', margin: '0 auto', padding: '20px 0 48px', marginBottom: '64px', }; const h1 = { color: '#333', fontSize: '24px', fontWeight: 'bold', margin: '40px 0', padding: '0', }; const text = { color: '#333', fontSize: '16px', lineHeight: '26px', }; const buttonContainer = { padding: '27px 0 27px', }; const button = { backgroundColor: '#5469d4', borderRadius: '5px', color: '#fff', fontSize: '16px', fontWeight: 'bold', textDecoration: 'none', textAlign: 'center' as const, display: 'block', padding: '12px 20px', };

Render and Send Email Template

// app/api/send-welcome/route.ts import { NextRequest } from 'next/server'; import { render } from '@react-email/render'; import WelcomeEmail from '@/emails/WelcomeEmail'; import { sendEmailWithResend } from '@/lib/resend'; export async function POST(request: NextRequest) { try { const { email, username } = await request.json(); const emailHtml = render( WelcomeEmail({ username, loginUrl: 'https://yourapp.com/login', }) ); await sendEmailWithResend({ to: email, subject: 'Welcome to Our Platform!', html: emailHtml, }); return Response.json({ success: true }); } catch (error) { return Response.json( { error: 'Failed to send welcome email' }, { status: 500 } ); } }

Notification System Architecture

Design a flexible notification system supporting multiple channels:

Database Schema

// prisma/schema.prisma model Notification { id String @id @default(cuid()) userId String type String // 'info', 'success', 'warning', 'error' title String message String read Boolean @default(false) actionUrl String? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) } model NotificationPreference { id String @id @default(cuid()) userId String email Boolean @default(true) push Boolean @default(true) sms Boolean @default(false) user User @relation(fields: [userId], references: [id]) }

Notification Service

// lib/notifications.ts import { prisma } from '@/lib/prisma'; import { sendEmailWithResend } from '@/lib/resend'; export type NotificationType = 'info' | 'success' | 'warning' | 'error'; export async function createNotification({ userId, type, title, message, actionUrl, sendEmail = false, }: { userId: string; type: NotificationType; title: string; message: string; actionUrl?: string; sendEmail?: boolean; }) { // Create in-app notification const notification = await prisma.notification.create({ data: { userId, type, title, message, actionUrl, }, }); // Check user preferences const preferences = await prisma.notificationPreference.findUnique({ where: { userId }, }); // Send email if enabled if (sendEmail && preferences?.email) { const user = await prisma.user.findUnique({ where: { id: userId }, }); if (user?.email) { await sendEmailWithResend({ to: user.email, subject: title, html: ` <h2>${title}</h2> <p>${message}</p> ${actionUrl ? `<a href="${actionUrl}">View Details</a>` : ''} `, }); } } return notification; } export async function markAsRead(notificationId: string) { return await prisma.notification.update({ where: { id: notificationId }, data: { read: true }, }); } export async function getUnreadCount(userId: string) { return await prisma.notification.count({ where: { userId, read: false, }, }); }

Notification API Routes

// app/api/notifications/route.ts import { NextRequest } from 'next/server'; import { getServerSession } from 'next-auth'; import { prisma } from '@/lib/prisma'; export async function GET(request: NextRequest) { const session = await getServerSession(); if (!session?.user?.id) { return Response.json({ error: 'Unauthorized' }, { status: 401 }); } const notifications = await prisma.notification.findMany({ where: { userId: session.user.id }, orderBy: { createdAt: 'desc' }, take: 20, }); return Response.json({ notifications }); } // app/api/notifications/[id]/read/route.ts export async function POST( request: NextRequest, { params }: { params: { id: string } } ) { const session = await getServerSession(); if (!session?.user?.id) { return Response.json({ error: 'Unauthorized' }, { status: 401 }); } await prisma.notification.update({ where: { id: params.id, userId: session.user.id, }, data: { read: true }, }); return Response.json({ success: true }); }

In-App Notifications Component

Build a notification dropdown for the UI:

// components/NotificationBell.tsx 'use client'; import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; interface Notification { id: string; type: string; title: string; message: string; read: boolean; actionUrl?: string; createdAt: string; } export default function NotificationBell() { const [notifications, setNotifications] = useState<Notification[]>([]); const [isOpen, setIsOpen] = useState(false); const [unreadCount, setUnreadCount] = useState(0); const router = useRouter(); useEffect(() => { fetchNotifications(); }, []); const fetchNotifications = async () => { const response = await fetch('/api/notifications'); const data = await response.json(); setNotifications(data.notifications); setUnreadCount(data.notifications.filter((n: Notification) => !n.read).length); }; const markAsRead = async (id: string) => { await fetch(`/api/notifications/${id}/read`, { method: 'POST' }); setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)) ); setUnreadCount((prev) => Math.max(0, prev - 1)); }; const handleNotificationClick = (notification: Notification) => { markAsRead(notification.id); if (notification.actionUrl) { router.push(notification.actionUrl); } setIsOpen(false); }; return ( <div style={{ position: 'relative' }}> <button onClick={() => setIsOpen(!isOpen)}> 🔔 {unreadCount > 0 && ( <span style={{ position: 'absolute', top: '-5px', right: '-5px', background: 'red', color: 'white', borderRadius: '50%', width: '20px', height: '20px', fontSize: '12px', }}> {unreadCount} </span> )} </button> {isOpen && ( <div style={{ position: 'absolute', top: '100%', right: 0, width: '350px', maxHeight: '400px', overflow: 'auto', background: 'white', border: '1px solid #ccc', borderRadius: '8px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', }}> {notifications.length === 0 ? ( <p style={{ padding: '20px', textAlign: 'center' }}> No notifications </p> ) : ( notifications.map((notification) => ( <div key={notification.id} onClick={() => handleNotificationClick(notification)} style={{ padding: '12px', borderBottom: '1px solid #eee', cursor: 'pointer', background: notification.read ? 'white' : '#f0f0f0', }} > <strong>{notification.title}</strong> <p style={{ margin: '5px 0', fontSize: '14px' }}> {notification.message} </p> <small style={{ color: '#666' }}> {new Date(notification.createdAt).toLocaleString()} </small> </div> )) )} </div> )} </div> ); }

Background Jobs with BullMQ

Process emails asynchronously with job queues:

Setup BullMQ

# Install BullMQ and Redis npm install bullmq ioredis # Docker Redis (for development) docker run -d -p 6379:6379 redis # Environment variable REDIS_URL=redis://localhost:6379

Email Queue Setup

// lib/queue.ts import { Queue, Worker } from 'bullmq'; import IORedis from 'ioredis'; import { sendEmailWithResend } from './resend'; const connection = new IORedis(process.env.REDIS_URL!); export const emailQueue = new Queue('email', { connection }); // Email worker export const emailWorker = new Worker( 'email', async (job) => { const { to, subject, html } = job.data; await sendEmailWithResend({ to, subject, html }); console.log(`Email sent to ${to}`); }, { connection } ); emailWorker.on('completed', (job) => { console.log(`Job ${job.id} completed`); }); emailWorker.on('failed', (job, err) => { console.error(`Job ${job?.id} failed:`, err); });

Queue Email Jobs

// app/api/queue-email/route.ts import { NextRequest } from 'next/server'; import { emailQueue } from '@/lib/queue'; export async function POST(request: NextRequest) { try { const { to, subject, html } = await request.json(); await emailQueue.add('send-email', { to, subject, html, }); return Response.json({ success: true, message: 'Email queued' }); } catch (error) { return Response.json( { error: 'Failed to queue email' }, { status: 500 } ); } }

Scheduled Emails

Send emails at specific times or intervals:

// lib/scheduled-emails.ts import { emailQueue } from './queue'; export async function scheduleEmail({ to, subject, html, sendAt, }: { to: string; subject: string; html: string; sendAt: Date; }) { const delay = sendAt.getTime() - Date.now(); await emailQueue.add( 'send-email', { to, subject, html }, { delay } ); } // Send daily digest export async function scheduleDailyDigest(userId: string) { const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(9, 0, 0, 0); // 9 AM await emailQueue.add( 'daily-digest', { userId }, { delay: tomorrow.getTime() - Date.now(), repeat: { pattern: '0 9 * * *' }, // Cron: daily at 9 AM } ); }
Email Best Practices: Always validate email addresses. Implement unsubscribe functionality. Use queues for bulk emails. Monitor delivery rates and bounce rates. Provide plain text alternatives. Test emails across different clients.

Exercise: Build a Notification System

  1. Set up email sending with Resend or Nodemailer
  2. Create React Email templates for welcome, reset password, and notification emails
  3. Build a database schema for notifications and preferences
  4. Implement an in-app notification bell component with unread count
  5. Create API routes for fetching and marking notifications as read
  6. Set up BullMQ for background email processing
  7. Implement scheduled emails (daily digest, reminders)
  8. Add notification preferences page where users can customize settings
Production Considerations: Implement rate limiting on email sending. Handle bounces and complaints. Store email delivery logs. Comply with anti-spam laws (CAN-SPAM, GDPR). Monitor queue health and retry failed jobs. Use separate queues for different priorities.