13 KiB
Advanced Backend Features
This reference provides detailed implementation guidance for advanced backend features beyond basic CRUD operations.
Multi-Tenant Architecture
When to Use
When workflow indicates multiple organizations/tenants need to share the same application instance with data isolation.
Implementation Strategy
1. Database Schema Modifications
Add tenant context to all tables:
ALTER TABLE users ADD COLUMN tenant_id UUID NOT NULL;
ALTER TABLE posts ADD COLUMN tenant_id UUID NOT NULL;
ALTER TABLE products ADD COLUMN tenant_id UUID NOT NULL;
-- Create index for performance
CREATE INDEX idx_users_tenant ON users(tenant_id);
CREATE INDEX idx_posts_tenant ON posts(tenant_id);
2. Tenant Identification Middleware
// Express.js
const tenantMiddleware = async (req, res, next) => {
// Extract tenant from subdomain, header, or token
const tenant = req.subdomains[0] ||
req.headers['x-tenant-id'] ||
req.user?.tenantId;
if (!tenant) {
return res.status(400).json({ error: 'Tenant not specified' });
}
req.tenant = await Tenant.findById(tenant);
next();
};
3. Tenant-Scoped Queries
// Automatically scope all queries to current tenant
class TenantModel {
static async find(query) {
return db.query({
...query,
tenant_id: req.tenant.id
});
}
}
4. Tenant Management API
Generate endpoints for:
POST /api/tenants- Create new tenantGET /api/tenants/:id- Get tenant infoPUT /api/tenants/:id/settings- Update settingsGET /api/tenants/:id/usage- Usage statistics
Real-Time Features
When to Use
When frontend has WebSocket connections, live updates, chat, notifications, or collaborative editing.
Implementation Options
Option 1: Socket.IO (Recommended for Node.js)
Setup:
const io = require('socket.io')(server, {
cors: { origin: process.env.FRONTEND_URL }
});
// Authentication
io.use(async (socket, next) => {
const token = socket.handshake.auth.token;
const user = await verifyToken(token);
socket.user = user;
next();
});
// Handle connections
io.on('connection', (socket) => {
console.log(`User ${socket.user.id} connected`);
// Join user's room
socket.join(`user:${socket.user.id}`);
// Handle events
socket.on('message', async (data) => {
await Message.create({ ...data, userId: socket.user.id });
io.to(`room:${data.roomId}`).emit('message', data);
});
});
Patterns:
// Broadcast to specific user
io.to(`user:${userId}`).emit('notification', data);
// Broadcast to room
io.to(`room:${roomId}`).emit('message', data);
// Broadcast to all except sender
socket.broadcast.emit('user-joined', socket.user);
Option 2: Server-Sent Events (SSE)
For one-way updates:
app.get('/api/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const sendEvent = (data) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
// Subscribe to events
eventEmitter.on('update', sendEvent);
req.on('close', () => {
eventEmitter.off('update', sendEvent);
});
});
Option 3: WebSocket (Native)
For Python FastAPI:
from fastapi import WebSocket
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
try:
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Echo: {data}")
except WebSocketDisconnect:
print("Client disconnected")
Connection Management
Track active connections:
const activeConnections = new Map();
io.on('connection', (socket) => {
activeConnections.set(socket.user.id, socket);
socket.on('disconnect', () => {
activeConnections.delete(socket.user.id);
});
});
// Send to specific user
function notifyUser(userId, event, data) {
const socket = activeConnections.get(userId);
if (socket) {
socket.emit(event, data);
}
}
File Upload Handling
When to Use
When frontend has file upload forms, image uploads, document attachments, or bulk imports.
Implementation Strategy
1. Basic Upload Handler
Express with Multer:
const multer = require('multer');
const path = require('path');
const storage = multer.diskStorage({
destination: './uploads/',
filename: (req, file, cb) => {
const uniqueName = `${Date.now()}-${file.originalname}`;
cb(null, uniqueName);
}
});
const upload = multer({
storage: storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|pdf/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (extname && mimetype) {
cb(null, true);
} else {
cb(new Error('Invalid file type'));
}
}
});
app.post('/api/upload', upload.single('file'), async (req, res) => {
const fileRecord = await File.create({
filename: req.file.filename,
originalName: req.file.originalname,
size: req.file.size,
mimetype: req.file.mimetype,
userId: req.user.id
});
res.json({ success: true, file: fileRecord });
});
2. Cloud Storage (AWS S3)
Direct upload with presigned URLs:
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
app.post('/api/upload/presign', async (req, res) => {
const { filename, contentType } = req.body;
const key = `uploads/${req.user.id}/${Date.now()}-${filename}`;
const presignedUrl = s3.getSignedUrl('putObject', {
Bucket: process.env.S3_BUCKET,
Key: key,
ContentType: contentType,
Expires: 300 // 5 minutes
});
res.json({
uploadUrl: presignedUrl,
fileKey: key
});
});
// After client uploads to S3
app.post('/api/upload/confirm', async (req, res) => {
const { fileKey } = req.body;
const fileUrl = `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${fileKey}`;
await File.create({
key: fileKey,
url: fileUrl,
userId: req.user.id
});
res.json({ success: true, url: fileUrl });
});
3. Image Processing
Resize and optimize:
const sharp = require('sharp');
app.post('/api/upload/image', upload.single('image'), async (req, res) => {
const processedPath = `processed-${req.file.filename}`;
await sharp(req.file.path)
.resize(800, 800, { fit: 'inside' })
.jpeg({ quality: 80 })
.toFile(path.join('./uploads/', processedPath));
res.json({
original: req.file.filename,
processed: processedPath
});
});
4. Multiple File Upload
app.post('/api/upload/multiple', upload.array('files', 10), async (req, res) => {
const files = await Promise.all(
req.files.map(file => File.create({
filename: file.filename,
originalName: file.originalname,
size: file.size,
userId: req.user.id
}))
);
res.json({ success: true, files });
});
Security Considerations
- Validate file types - Check both extension and MIME type
- Limit file size - Prevent DoS attacks
- Scan for malware - Use ClamAV or similar
- Generate unique filenames - Prevent overwrites
- Store outside web root - Prevent direct access
- Implement rate limiting - Limit uploads per user
Background Jobs
When to Use
When workflow has async operations: email sending, report generation, data processing, scheduled tasks.
Implementation Options
Option 1: Bull (Node.js with Redis)
Setup:
const Queue = require('bull');
const emailQueue = new Queue('email', {
redis: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
}
});
// Add job to queue
app.post('/api/send-email', async (req, res) => {
await emailQueue.add({
to: req.body.email,
subject: 'Welcome',
body: 'Thank you for signing up'
});
res.json({ success: true, message: 'Email queued' });
});
// Process jobs
emailQueue.process(async (job) => {
const { to, subject, body } = job.data;
await sendEmail(to, subject, body);
});
// Handle job events
emailQueue.on('completed', (job) => {
console.log(`Job ${job.id} completed`);
});
emailQueue.on('failed', (job, err) => {
console.error(`Job ${job.id} failed:`, err);
});
Job priorities and delays:
// High priority
await emailQueue.add(data, { priority: 1 });
// Delayed job
await emailQueue.add(data, { delay: 60000 }); // 1 minute
// Scheduled job
await emailQueue.add(data, {
repeat: { cron: '0 9 * * *' } // Daily at 9 AM
});
Option 2: Celery (Python)
Setup:
from celery import Celery
app = Celery('tasks', broker='redis://localhost:6379')
@app.task
def send_email(to, subject, body):
# Send email logic
pass
# Call async
send_email.delay('user@example.com', 'Hello', 'Welcome')
# Call with delay
send_email.apply_async(
args=['user@example.com', 'Hello', 'Welcome'],
countdown=60 # 60 seconds
)
# Periodic task
from celery.schedules import crontab
@app.task
def cleanup_old_files():
# Cleanup logic
pass
app.conf.beat_schedule = {
'cleanup-daily': {
'task': 'cleanup_old_files',
'schedule': crontab(hour=2, minute=0)
}
}
Common Job Patterns
1. Email Notifications
const sendWelcomeEmail = async (userId) => {
await emailQueue.add({ userId, template: 'welcome' });
};
const sendDigest = async () => {
const users = await User.findActiveSubscribers();
for (const user of users) {
await emailQueue.add({
userId: user.id,
template: 'digest'
});
}
};
2. Report Generation
const generateReport = async (reportId) => {
await reportQueue.add({ reportId }, {
timeout: 300000, // 5 minutes
attempts: 3
});
};
reportQueue.process(async (job) => {
const { reportId } = job.data;
const data = await fetchReportData(reportId);
const pdf = await generatePDF(data);
await uploadToS3(pdf, reportId);
await Report.update(reportId, { status: 'completed' });
});
3. Data Import
const importCSV = async (fileId) => {
await importQueue.add({ fileId }, {
attempts: 1, // Don't retry
timeout: 600000 // 10 minutes
});
};
importQueue.process(async (job) => {
const { fileId } = job.data;
const file = await getFile(fileId);
const rows = await parseCSV(file);
for (let i = 0; i < rows.length; i++) {
await importRow(rows[i]);
job.progress((i / rows.length) * 100);
}
});
4. Scheduled Tasks
// Daily cleanup
const cleanupQueue = new Queue('cleanup');
cleanupQueue.add({}, {
repeat: { cron: '0 2 * * *' } // 2 AM daily
});
cleanupQueue.process(async () => {
await cleanupExpiredSessions();
await deleteOldLogs();
await optimizeDatabase();
});
Monitoring and Debugging
Job status endpoint:
app.get('/api/jobs/:id', async (req, res) => {
const job = await emailQueue.getJob(req.params.id);
res.json({
id: job.id,
progress: job.progress(),
state: await job.getState(),
failedReason: job.failedReason,
finishedOn: job.finishedOn
});
});
// Queue statistics
app.get('/api/queue/stats', async (req, res) => {
const [waiting, active, completed, failed] = await Promise.all([
emailQueue.getWaitingCount(),
emailQueue.getActiveCount(),
emailQueue.getCompletedCount(),
emailQueue.getFailedCount()
]);
res.json({ waiting, active, completed, failed });
});
Error Handling
emailQueue.process(async (job) => {
try {
await sendEmail(job.data);
} catch (error) {
// Log error
console.error(`Job ${job.id} failed:`, error);
// Notify admin on critical errors
if (error.code === 'SMTP_ERROR') {
await notifyAdmin(`Email system down: ${error.message}`);
}
throw error; // Re-throw to mark job as failed
}
});
// Retry failed jobs
emailQueue.on('failed', async (job, err) => {
if (job.attemptsMade < 3) {
await job.retry();
}
});
Best Practices Summary
- Multi-Tenant: Always validate tenant context, use indexes, implement audit logging
- Real-Time: Handle disconnections gracefully, implement reconnection logic, validate all events
- File Upload: Validate types and sizes, use virus scanning, implement cleanup jobs
- Background Jobs: Set appropriate timeouts, implement retry logic, monitor queue health
Integration with Main Workflow
When generating backend code, detect these patterns in frontend/UI:
- Multiple organization selector → Multi-tenant
- Live chat/notifications → Real-time
- File upload forms → File handling
- "Process in background" → Background jobs
Auto-generate appropriate infrastructure and endpoints for detected patterns.