Build production-ready features: WebSockets, file uploads, payments, emails, and background jobs
Modern applications require more than basic CRUD operations. Real-time updates, file handling, payment processing, and email notifications are essential for production applications.
Transform your Task Manager into a collaborative platform with real-time updates, file attachments, premium subscriptions, and automated notifications.
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:
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:
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:
SendGrid delivers over 100 billion emails monthly for companies like Uber, Spotify, and Airbnb. Provides reliable transactional and marketing email infrastructure.
Key Features:
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:
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:
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.
# Backend
npm install socket.io
npm install @types/socket.io --save-dev
# Frontend
npm install socket.io-clientCreate 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;
}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}`);
});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();
}
}Events - Custom messages sent between client and server
Rooms - Groups of sockets for targeted broadcasting
Namespaces - Separate communication channels
Acknowledgments - Confirm message receipt
š Learn More:
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.
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' });
}
});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;
}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);
});S3 (Simple Storage Service) is scalable, durable, and cost-effective for storing files. It integrates with CloudFront CDN for fast global delivery.
In S3 bucket ā Permissions ā CORS configuration:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"AllowedOrigins": ["http://localhost:3000", "https://yourdomain.com"],
"ExposeHeaders": ["ETag"]
}
]npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner multer
npm install @types/multer --save-devCreate 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 });
}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;'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>
);
}⢠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
š Learn More:
Stripe is the leading payment platform with excellent developer experience, comprehensive documentation, and support for subscriptions, one-time payments, and more.
# Backend
npm install stripe
# Frontend
npm install @stripe/stripe-js @stripe/react-stripe-jsIn Stripe Dashboard ā Products, create plans:
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;'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>
);
}š Learn More:
SendGrid provides reliable email delivery for transactional emails like welcome messages, password resets, notifications, and receipts.
npm install @sendgrid/mailCreate 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);
}// 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);
});⢠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
š Learn More:
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.
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/npm install bull
npm install @types/bull --save-devCreate 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`);
});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);
});npm install @bull-board/express @bull-board/apiAdd 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 dashboardYou've mastered advanced full-stack features! Your application now has real-time updates, file uploads, payments, emails, and background processing.
š Additional Resources: