Back to Full-Stack Mastery

Module 5: Real-Time & Advanced Features

Build production-ready features: WebSockets, file uploads, payments, emails, and background jobs

šŸ“š Advanced Application Features

Modern applications require more than basic CRUD operations. Real-time updates, file handling, payment processing, and email notifications are essential for production applications.

Technologies We'll Use

Socket.io

Real-time bidirectional communication

socket.io →

Stripe

Payment processing and subscriptions

stripe.com/docs →

AWS S3

Scalable file storage and CDN

aws.amazon.com/s3 →

SendGrid

Email delivery and automation

sendgrid.com/docs →

Bull Queue

Redis-based job queue system

github.com/OptimalBits/bull →

Redis

In-memory data store for caching

redis.io/docs →

šŸŽÆ What We'll Build

Transform your Task Manager into a collaborative platform with real-time updates, file attachments, premium subscriptions, and automated notifications.

Advanced Features:

  • Real-time task updates with WebSockets
  • Live notifications and presence indicators
  • File uploads to AWS S3
  • Premium subscriptions with Stripe
  • Email notifications with SendGrid
  • Background jobs with Bull Queue

What You'll Learn:

• WebSocket implementation
• File upload handling
• Payment integration
• Email automation
• Background processing
• Real-time notifications
• Webhook handling
• Queue management

šŸ”§ Technology Deep Dive

Socket.io - Real-Time Engine

Socket.io enables real-time, bidirectional communication between web clients and servers. It uses WebSocket protocol with fallbacks to HTTP long-polling for older browsers.

Key Features:

  • • Automatic reconnection with exponential backoff
  • • Binary data support (ArrayBuffer, Blob)
  • • Room-based broadcasting for targeted messages
  • • Middleware support for authentication
  • • Built-in acknowledgments for reliable messaging
Official Documentation →

Stripe - Payment Infrastructure

Stripe provides a complete payment platform with APIs for accepting payments, managing subscriptions, and handling complex billing scenarios. Supports 135+ currencies and payment methods worldwide.

Key Features:

  • • PCI DSS Level 1 compliant (highest security)
  • • Subscription management with metered billing
  • • Webhook events for payment lifecycle
  • • Built-in fraud detection (Stripe Radar)
  • • Test mode with realistic card numbers

AWS S3 - Object Storage

Amazon S3 (Simple Storage Service) is industry-leading object storage with 99.999999999% (11 9's) durability. Designed to store and retrieve any amount of data from anywhere.

Key Features:

  • • Unlimited storage capacity with pay-as-you-go pricing
  • • Automatic data replication across multiple facilities
  • • Lifecycle policies for automatic archival/deletion
  • • Pre-signed URLs for secure temporary access
  • • Integration with CloudFront CDN for global delivery

SendGrid - Email Delivery Platform

SendGrid delivers over 100 billion emails monthly for companies like Uber, Spotify, and Airbnb. Provides reliable transactional and marketing email infrastructure.

Key Features:

  • • 99% delivery rate with real-time analytics
  • • Dynamic email templates with Handlebars
  • • Email validation API to reduce bounces
  • • Webhook events for opens, clicks, bounces
  • • Free tier: 100 emails/day forever

Bull Queue - Job Processing

Bull is a Redis-based queue system for Node.js that handles distributed job processing. Perfect for tasks that are too slow or resource-intensive for HTTP requests.

Key Features:

  • • Delayed jobs with precise scheduling
  • • Automatic retry with exponential backoff
  • • Job prioritization and rate limiting
  • • Progress tracking and event listeners
  • • Bull Board UI for monitoring and management

Redis - In-Memory Data Store

Redis is an open-source, in-memory data structure store used as a database, cache, and message broker. Sub-millisecond latency makes it perfect for real-time applications.

Key Features:

  • • Multiple data structures (strings, hashes, lists, sets, sorted sets)
  • • Pub/Sub messaging for real-time communication
  • • Persistence options (RDB snapshots, AOF logs)
  • • Atomic operations for race-condition-free updates
  • • Lua scripting for complex operations

Lesson 1: WebSocket Fundamentals with Socket.io

šŸ“– What are WebSockets?

WebSockets provide full-duplex communication between client and server. Unlike HTTP requests that are one-way, WebSockets maintain an open connection allowing real-time bidirectional data flow.

Use Cases: Chat applications, live notifications, collaborative editing, real-time dashboards, multiplayer games, and live data feeds.

Step 1: Install Socket.io

# Backend
npm install socket.io
npm install @types/socket.io --save-dev

# Frontend
npm install socket.io-client

Step 2: Setup Socket.io Server

Create src/socket/index.ts:

import { Server } from 'socket.io';
import { Server as HTTPServer } from 'http';

export function initializeSocket(httpServer: HTTPServer) {
  const io = new Server(httpServer, {
    cors: {
      origin: process.env.FRONTEND_URL || 'http://localhost:3000',
      methods: ['GET', 'POST'],
      credentials: true,
    },
  });

  // Connection event
  io.on('connection', (socket) => {
    console.log('User connected:', socket.id);

    // Join user to their personal room
    socket.on('join', (userId: string) => {
      socket.join(`user:${userId}`);
      console.log(`User ${userId} joined their room`);
    });

    // Handle disconnection
    socket.on('disconnect', () => {
      console.log('User disconnected:', socket.id);
    });
  });

  return io;
}

Step 3: Integrate with Express Server

Update src/index.ts:

import express from 'express';
import http from 'http';
import { initializeSocket } from './socket';

const app = express();
const httpServer = http.createServer(app);

// Initialize Socket.io
const io = initializeSocket(httpServer);

// Make io available in routes
app.set('io', io);

// Your existing middleware and routes...

const PORT = process.env.PORT || 3001;
httpServer.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Step 4: Connect from Frontend

Create lib/socket.ts in your Next.js app:

import { io, Socket } from 'socket.io-client';

let socket: Socket | null = null;

export function getSocket(): Socket {
  if (!socket) {
    socket = io(process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001', {
      autoConnect: false,
      withCredentials: true,
    });
  }
  return socket;
}

export function connectSocket(userId: string) {
  const socket = getSocket();
  
  if (!socket.connected) {
    socket.connect();
    socket.emit('join', userId);
  }
  
  return socket;
}

export function disconnectSocket() {
  if (socket?.connected) {
    socket.disconnect();
  }
}

šŸ’” Socket.io Concepts:

Events - Custom messages sent between client and server

Rooms - Groups of sockets for targeted broadcasting

Namespaces - Separate communication channels

Acknowledgments - Confirm message receipt

Lesson 2: Real-Time Task Updates

šŸ“– Building Collaborative Features

Enable multiple users to see task changes instantly. When one user updates a task, all other users viewing that task see the changes in real-time.

Step 1: Emit Task Events from Backend

Update your task routes to emit socket events:

// POST /api/tasks - Create task
router.post('/', async (req: Request, res: Response) => {
  try {
    const task = await prisma.task.create({
      data: req.body,
      include: { user: true, category: true },
    });
    
    // Emit to all connected clients
    const io = req.app.get('io');
    io.emit('task:created', task);
    
    res.status(201).json(task);
  } catch (error) {
    res.status(500).json({ error: 'Failed to create task' });
  }
});

// PATCH /api/tasks/:id - Update task
router.patch('/:id', async (req: Request, res: Response) => {
  try {
    const task = await prisma.task.update({
      where: { id: req.params.id },
      data: req.body,
      include: { user: true, category: true },
    });
    
    const io = req.app.get('io');
    io.emit('task:updated', task);
    
    res.json(task);
  } catch (error) {
    res.status(500).json({ error: 'Failed to update task' });
  }
});

// DELETE /api/tasks/:id - Delete task
router.delete('/:id', async (req: Request, res: Response) => {
  try {
    await prisma.task.delete({ where: { id: req.params.id } });
    
    const io = req.app.get('io');
    io.emit('task:deleted', { id: req.params.id });
    
    res.json({ message: 'Task deleted' });
  } catch (error) {
    res.status(500).json({ error: 'Failed to delete task' });
  }
});

Step 2: Listen for Events in Frontend

Create a React hook for real-time tasks:

import { useEffect, useState } from 'react';
import { getSocket } from '@/lib/socket';

export function useRealtimeTasks(initialTasks: Task[]) {
  const [tasks, setTasks] = useState(initialTasks);

  useEffect(() => {
    const socket = getSocket();

    // Listen for new tasks
    socket.on('task:created', (newTask: Task) => {
      setTasks((prev) => [newTask, ...prev]);
    });

    // Listen for task updates
    socket.on('task:updated', (updatedTask: Task) => {
      setTasks((prev) =>
        prev.map((task) =>
          task.id === updatedTask.id ? updatedTask : task
        )
      );
    });

    // Listen for task deletions
    socket.on('task:deleted', ({ id }: { id: string }) => {
      setTasks((prev) => prev.filter((task) => task.id !== id));
    });

    return () => {
      socket.off('task:created');
      socket.off('task:updated');
      socket.off('task:deleted');
    };
  }, []);

  return tasks;
}

Step 3: Add User Presence

Show who's currently online:

// Backend - track online users
const onlineUsers = new Map<string, string>(); // socketId -> userId

io.on('connection', (socket) => {
  socket.on('user:online', (userId: string) => {
    onlineUsers.set(socket.id, userId);
    io.emit('users:online', Array.from(onlineUsers.values()));
  });

  socket.on('disconnect', () => {
    onlineUsers.delete(socket.id);
    io.emit('users:online', Array.from(onlineUsers.values()));
  });
});

// Frontend - display online users
socket.on('users:online', (userIds: string[]) => {
  setOnlineUsers(userIds);
});

šŸŽÆ What You Learned:

  • āœ“ Broadcasting events to all clients
  • āœ“ Listening for real-time updates
  • āœ“ Optimistic UI updates
  • āœ“ User presence tracking

Lesson 3: File Uploads with AWS S3

šŸ“– Why AWS S3?

S3 (Simple Storage Service) is scalable, durable, and cost-effective for storing files. It integrates with CloudFront CDN for fast global delivery.

Step 1: Setup AWS S3 Bucket

  1. AWS Console → S3 → Create bucket
  2. Bucket name: task-manager-uploads-[unique-id]
  3. Region: Choose closest to your users
  4. Block all public access: Uncheck (we'll use signed URLs)
  5. Enable versioning (optional)
  6. Create bucket

Step 2: Configure CORS

In S3 bucket → Permissions → CORS configuration:

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
    "AllowedOrigins": ["http://localhost:3000", "https://yourdomain.com"],
    "ExposeHeaders": ["ETag"]
  }
]

Step 3: Install AWS SDK

npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner multer
npm install @types/multer --save-dev

Step 4: Create S3 Upload Service

Create src/services/s3.ts:

import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3Client = new S3Client({
  region: process.env.AWS_REGION!,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
});

const BUCKET_NAME = process.env.AWS_S3_BUCKET!;

export async function uploadFile(file: Express.Multer.File, folder: string) {
  const key = `${folder}/${Date.now()}-${file.originalname}`;
  
  const command = new PutObjectCommand({
    Bucket: BUCKET_NAME,
    Key: key,
    Body: file.buffer,
    ContentType: file.mimetype,
  });

  await s3Client.send(command);
  
  return {
    key,
    url: `https://${BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`,
  };
}

export async function deleteFile(key: string) {
  const command = new DeleteObjectCommand({
    Bucket: BUCKET_NAME,
    Key: key,
  });
  
  await s3Client.send(command);
}

export async function getSignedDownloadUrl(key: string, expiresIn = 3600) {
  const command = new GetObjectCommand({
    Bucket: BUCKET_NAME,
    Key: key,
  });
  
  return await getSignedUrl(s3Client, command, { expiresIn });
}

Step 5: Create Upload Route

Create src/routes/upload.ts:

import { Router } from 'express';
import multer from 'multer';
import { uploadFile } from '../services/s3';

const router = Router();

// Configure multer for memory storage
const upload = multer({
  storage: multer.memoryStorage(),
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB limit
  },
  fileFilter: (req, file, cb) => {
    // Allow images and PDFs
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
    if (allowedTypes.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error('Invalid file type'));
    }
  },
});

// POST /api/upload - Upload file
router.post('/', upload.single('file'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'No file provided' });
    }

    const result = await uploadFile(req.file, 'task-attachments');
    
    res.json({
      message: 'File uploaded successfully',
      file: {
        key: result.key,
        url: result.url,
        name: req.file.originalname,
        size: req.file.size,
        type: req.file.mimetype,
      },
    });
  } catch (error) {
    console.error('Upload error:', error);
    res.status(500).json({ error: 'Failed to upload file' });
  }
});

export default router;

Step 6: Frontend Upload Component

'use client';

import { useState } from 'react';

export function FileUpload() {
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);

  async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (!file) return;

    setUploading(true);
    const formData = new FormData();
    formData.append('file', file);

    try {
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData,
      });

      const data = await response.json();
      console.log('Uploaded:', data.file);
      
      // Save file info to task
      // await updateTask(taskId, { attachments: [...attachments, data.file] });
    } catch (error) {
      console.error('Upload failed:', error);
    } finally {
      setUploading(false);
    }
  }

  return (
    <div>
      <input
        type="file"
        onChange={handleUpload}
        disabled={uploading}
        accept="image/*,.pdf"
      />
      {uploading && <p>Uploading... {progress}%</p>}
    </div>
  );
}

šŸ’” Best Practices:

• Validate file types and sizes on both client and server

• Use signed URLs for secure access

• Implement virus scanning for production

• Optimize images before upload (resize, compress)

• Set lifecycle policies to delete old files

Lesson 4: Payment Integration with Stripe

šŸ“– Why Stripe?

Stripe is the leading payment platform with excellent developer experience, comprehensive documentation, and support for subscriptions, one-time payments, and more.

Step 1: Setup Stripe Account

  1. Sign up at stripe.com
  2. Get API keys from Dashboard → Developers → API keys
  3. Use test keys for development
  4. Add to .env: STRIPE_SECRET_KEY and NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY

Step 2: Install Stripe SDK

# Backend
npm install stripe

# Frontend
npm install @stripe/stripe-js @stripe/react-stripe-js

Step 3: Create Subscription Plans

In Stripe Dashboard → Products, create plans:

  • Free: $0/month - 10 tasks limit
  • Pro: $9/month - Unlimited tasks, file uploads
  • Team: $29/month - Everything + team collaboration

Step 4: Create Checkout Session

Create src/routes/stripe.ts:

import { Router } from 'express';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
});

const router = Router();

// POST /api/stripe/create-checkout-session
router.post('/create-checkout-session', async (req, res) => {
  try {
    const { priceId, userId } = req.body;

    const session = await stripe.checkout.sessions.create({
      mode: 'subscription',
      payment_method_types: ['card'],
      line_items: [
        {
          price: priceId,
          quantity: 1,
        },
      ],
      success_url: `${process.env.FRONTEND_URL}/dashboard?success=true`,
      cancel_url: `${process.env.FRONTEND_URL}/pricing?canceled=true`,
      client_reference_id: userId,
      metadata: {
        userId,
      },
    });

    res.json({ sessionId: session.id, url: session.url });
  } catch (error) {
    console.error('Stripe error:', error);
    res.status(500).json({ error: 'Failed to create checkout session' });
  }
});

// POST /api/stripe/webhook - Handle Stripe events
router.post('/webhook', async (req, res) => {
  const sig = req.headers['stripe-signature'] as string;
  
  try {
    const event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    );

    switch (event.type) {
      case 'checkout.session.completed':
        const session = event.data.object;
        // Update user subscription in database
        await prisma.user.update({
          where: { id: session.metadata.userId },
          data: {
            subscriptionStatus: 'active',
            stripeCustomerId: session.customer as string,
            stripeSubscriptionId: session.subscription as string,
          },
        });
        break;

      case 'customer.subscription.deleted':
        const subscription = event.data.object;
        // Cancel user subscription
        await prisma.user.update({
          where: { stripeSubscriptionId: subscription.id },
          data: { subscriptionStatus: 'canceled' },
        });
        break;
    }

    res.json({ received: true });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(400).send(`Webhook Error: ${error.message}`);
  }
});

export default router;

Step 5: Frontend Checkout

'use client';

import { loadStripe } from '@stripe/stripe-js';

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);

export function PricingCard({ plan }) {
  async function handleSubscribe() {
    const response = await fetch('/api/stripe/create-checkout-session', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        priceId: plan.stripePriceId,
        userId: currentUser.id,
      }),
    });

    const { url } = await response.json();
    window.location.href = url;
  }

  return (
    <div className="border rounded-lg p-6">
      <h3 className="text-2xl font-bold">{plan.name}</h3>
      <p className="text-3xl font-bold my-4">${plan.price}/mo</p>
      <button
        onClick={handleSubscribe}
        className="w-full bg-blue-500 text-white py-2 rounded"
      >
        Subscribe
      </button>
    </div>
  );
}

šŸŽÆ What You Learned:

  • āœ“ Creating Stripe checkout sessions
  • āœ“ Handling subscription webhooks
  • āœ“ Managing subscription status
  • āœ“ Secure payment processing

Lesson 5: Email Automation with SendGrid

šŸ“– Transactional Emails

SendGrid provides reliable email delivery for transactional emails like welcome messages, password resets, notifications, and receipts.

Step 1: Setup SendGrid

  1. Sign up at sendgrid.com
  2. Verify your sender email address
  3. Create API key: Settings → API Keys → Create API Key
  4. Add to .env: SENDGRID_API_KEY

Step 2: Install SendGrid SDK

npm install @sendgrid/mail

Step 3: Create Email Service

Create src/services/email.ts:

import sgMail from '@sendgrid/mail';

sgMail.setApiKey(process.env.SENDGRID_API_KEY!);

const FROM_EMAIL = process.env.FROM_EMAIL || 'noreply@taskmanager.com';

export async function sendWelcomeEmail(to: string, name: string) {
  const msg = {
    to,
    from: FROM_EMAIL,
    subject: 'Welcome to Task Manager!',
    text: `Hi ${name}, welcome to Task Manager!`,
    html: `
      <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
        <h1 style="color: #3B82F6;">Welcome to Task Manager!</h1>
        <p>Hi ${name},</p>
        <p>Thanks for signing up! We're excited to help you stay organized.</p>
        <p>Get started by creating your first task.</p>
        <a href="${process.env.FRONTEND_URL}/dashboard" 
           style="display: inline-block; padding: 12px 24px; background: #3B82F6; 
                  color: white; text-decoration: none; border-radius: 6px; margin: 20px 0;">
          Go to Dashboard
        </a>
        <p>Best regards,<br>The Task Manager Team</p>
      </div>
    `,
  };

  try {
    await sgMail.send(msg);
    console.log('Welcome email sent to:', to);
  } catch (error) {
    console.error('Email error:', error);
    throw error;
  }
}

export async function sendTaskAssignedEmail(
  to: string,
  taskTitle: string,
  assignedBy: string
) {
  const msg = {
    to,
    from: FROM_EMAIL,
    subject: `New Task Assigned: ${taskTitle}`,
    html: `
      <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
        <h2>You've been assigned a new task</h2>
        <div style="background: #F3F4F6; padding: 16px; border-radius: 8px; margin: 20px 0;">
          <h3 style="margin: 0 0 8px 0;">${taskTitle}</h3>
          <p style="margin: 0; color: #6B7280;">Assigned by: ${assignedBy}</p>
        </div>
        <a href="${process.env.FRONTEND_URL}/tasks" 
           style="display: inline-block; padding: 12px 24px; background: #3B82F6; 
                  color: white; text-decoration: none; border-radius: 6px;">
          View Task
        </a>
      </div>
    `,
  };

  await sgMail.send(msg);
}

export async function sendTaskReminderEmail(
  to: string,
  taskTitle: string,
  dueDate: Date
) {
  const msg = {
    to,
    from: FROM_EMAIL,
    subject: `Reminder: ${taskTitle} is due soon`,
    html: `
      <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
        <h2>ā° Task Reminder</h2>
        <p>Your task is due soon:</p>
        <div style="background: #FEF3C7; padding: 16px; border-radius: 8px; margin: 20px 0;">
          <h3 style="margin: 0 0 8px 0;">${taskTitle}</h3>
          <p style="margin: 0; color: #92400E;">
            Due: ${dueDate.toLocaleDateString()} at ${dueDate.toLocaleTimeString()}
          </p>
        </div>
        <a href="${process.env.FRONTEND_URL}/tasks" 
           style="display: inline-block; padding: 12px 24px; background: #F59E0B; 
                  color: white; text-decoration: none; border-radius: 6px;">
          Complete Task
        </a>
      </div>
    `,
  };

  await sgMail.send(msg);
}

Step 4: Use Email Service

// In your registration route
import { sendWelcomeEmail } from '../services/email';

router.post('/register', async (req, res) => {
  const user = await prisma.user.create({
    data: { ...req.body },
  });

  // Send welcome email (don't await - send in background)
  sendWelcomeEmail(user.email, user.name).catch(console.error);

  res.json(user);
});

// When assigning a task
router.post('/tasks/:id/assign', async (req, res) => {
  const { userId } = req.body;
  
  const task = await prisma.task.update({
    where: { id: req.params.id },
    data: { userId },
    include: { user: true, assignedBy: true },
  });

  sendTaskAssignedEmail(
    task.user.email,
    task.title,
    task.assignedBy.name
  ).catch(console.error);

  res.json(task);
});

šŸ’” Email Best Practices:

• Use email templates for consistent branding

• Include unsubscribe links for marketing emails

• Test emails before sending to users

• Monitor delivery rates and bounces

• Don't block API responses waiting for emails

Lesson 6: Background Jobs with Bull Queue

šŸ“– Why Background Jobs?

Some tasks take too long to run during an API request: sending emails, processing images, generating reports. Background jobs handle these asynchronously without blocking your API.

Step 1: Setup Redis

Bull Queue requires Redis for job storage:

# Install Redis locally (macOS)
brew install redis
brew services start redis

# Or use Docker
docker run -d -p 6379:6379 redis:alpine

# Or use Redis Cloud (free tier)
# https://redis.com/try-free/

Step 2: Install Bull

npm install bull
npm install @types/bull --save-dev

Step 3: Create Queue

Create src/queues/email.queue.ts:

import Queue from 'bull';
import { sendWelcomeEmail, sendTaskReminderEmail } from '../services/email';

// Create email queue
export const emailQueue = new Queue('email', {
  redis: {
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT || '6379'),
  },
});

// Process jobs
emailQueue.process('welcome', async (job) => {
  const { email, name } = job.data;
  await sendWelcomeEmail(email, name);
  return { sent: true };
});

emailQueue.process('task-reminder', async (job) => {
  const { email, taskTitle, dueDate } = job.data;
  await sendTaskReminderEmail(email, taskTitle, new Date(dueDate));
  return { sent: true };
});

// Job event listeners
emailQueue.on('completed', (job, result) => {
  console.log(`Job ${job.id} completed:`, result);
});

emailQueue.on('failed', (job, err) => {
  console.error(`Job ${job.id} failed:`, err);
});

emailQueue.on('stalled', (job) => {
  console.warn(`Job ${job.id} stalled`);
});

Step 4: Add Jobs to Queue

import { emailQueue } from '../queues/email.queue';

// In registration route
router.post('/register', async (req, res) => {
  const user = await prisma.user.create({
    data: { ...req.body },
  });

  // Add job to queue instead of sending directly
  await emailQueue.add('welcome', {
    email: user.email,
    name: user.name,
  });

  res.json(user);
});

// Schedule reminder for task due in 24 hours
router.post('/tasks', async (req, res) => {
  const task = await prisma.task.create({
    data: { ...req.body },
    include: { user: true },
  });

  if (task.dueDate) {
    const reminderTime = new Date(task.dueDate);
    reminderTime.setHours(reminderTime.getHours() - 24);

    await emailQueue.add(
      'task-reminder',
      {
        email: task.user.email,
        taskTitle: task.title,
        dueDate: task.dueDate,
      },
      {
        delay: reminderTime.getTime() - Date.now(),
      }
    );
  }

  res.json(task);
});

Step 5: Monitor Jobs with Bull Board

npm install @bull-board/express @bull-board/api

Add to your Express app:

import { createBullBoard } from '@bull-board/api';
import { BullAdapter } from '@bull-board/api/bullAdapter';
import { ExpressAdapter } from '@bull-board/express';
import { emailQueue } from './queues/email.queue';

const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath('/admin/queues');

createBullBoard({
  queues: [new BullAdapter(emailQueue)],
  serverAdapter,
});

app.use('/admin/queues', serverAdapter.getRouter());

// Visit http://localhost:3001/admin/queues to see dashboard

šŸŽ‰ Congratulations!

You've mastered advanced full-stack features! Your application now has real-time updates, file uploads, payments, emails, and background processing.

What You've Accomplished:
āœ“ WebSocket real-time updates
āœ“ AWS S3 file uploads
āœ“ Stripe payment integration
āœ“ SendGrid email automation
āœ“ Bull Queue background jobs
āœ“ Job monitoring dashboard
āœ“ Production-ready features
āœ“ Scalable architecture

šŸ“š Additional Resources:

šŸš€ Next Steps

  • →Add push notifications with Firebase Cloud Messaging
  • →Implement full-text search with Elasticsearch
  • →Add video processing with AWS MediaConvert
  • →Build GraphQL API with Apollo Server
  • →Implement rate limiting with Redis
  • →Add analytics with Mixpanel or Amplitude