/** * API Response Optimization Middleware * Implements response batching, field filtering, and pagination */ const logger = require("../config/logger"); /** * Enable response compression for API endpoints */ const enableCompression = (req, res, next) => { // Already handled by global compression middleware next(); }; /** * Add cache headers for GET requests * SAFEGUARD: Checks headers not already sent before setting */ const addCacheHeaders = (maxAge = 300) => { return (req, res, next) => { if (req.method === "GET" && !res.headersSent) { try { res.set({ "Cache-Control": `public, max-age=${maxAge}`, Vary: "Accept-Encoding", }); } catch (error) { logger.warn("Failed to set cache headers", { error: error.message }); } } next(); }; }; /** * Field filtering middleware * Allows clients to request only specific fields: ?fields=id,name,price * SAFEGUARD: Validates field names to prevent injection attacks */ const fieldFilter = (req, res, next) => { const originalJson = res.json.bind(res); res.json = function (data) { const fields = req.query.fields; if (!fields || !data || res.headersSent) { return originalJson(data); } try { // SAFEGUARD: Validate field names (alphanumeric, underscore, dot only) if (!/^[a-zA-Z0-9_.,\s]+$/.test(fields)) { logger.warn("Invalid field filter attempted", { fields }); return originalJson(data); } const fieldList = fields .split(",") .map((f) => f.trim()) .filter(Boolean); // SAFEGUARD: Limit number of fields if (fieldList.length > 50) { logger.warn("Too many fields requested", { count: fieldList.length }); return originalJson(data); } const filterObject = (obj) => { if (!obj || typeof obj !== "object") return obj; const filtered = {}; fieldList.forEach((field) => { if (field in obj) { filtered[field] = obj[field]; } }); return filtered; }; if (Array.isArray(data)) { data = data.map(filterObject); } else if (data.success !== undefined && data.data) { // Handle wrapped responses if (Array.isArray(data.data)) { data.data = data.data.map(filterObject); } else { data.data = filterObject(data.data); } } else { data = filterObject(data); } return originalJson(data); } catch (error) { logger.error("Field filter error", { error: error.message }); return originalJson(data); } }; next(); }; /** * Pagination middleware * Adds pagination support: ?page=1&limit=20 */ const paginate = (defaultLimit = 20, maxLimit = 100) => { return (req, res, next) => { const page = Math.max(1, parseInt(req.query.page) || 1); const limit = Math.min( maxLimit, Math.max(1, parseInt(req.query.limit) || defaultLimit) ); const offset = (page - 1) * limit; req.pagination = { page, limit, offset, maxLimit, }; // Helper to add pagination info to response res.paginate = (data, total) => { const totalPages = Math.ceil(total / limit); return res.json({ success: true, data, pagination: { page, limit, total, totalPages, hasNext: page < totalPages, hasPrev: page > 1, }, }); }; next(); }; }; /** * Response time tracking * SAFEGUARD: Checks headers not sent before setting X-Response-Time header */ const trackResponseTime = (req, res, next) => { const start = Date.now(); res.on("finish", () => { const duration = Date.now() - start; // Log slow requests if (duration > 1000) { logger.warn("Slow API request", { method: req.method, path: req.path, duration: `${duration}ms`, status: res.statusCode, }); } // Add response time header only if headers haven't been sent if (!res.headersSent) { try { res.set("X-Response-Time", `${duration}ms`); } catch (error) { logger.debug("Could not set X-Response-Time header", { error: error.message, }); } } }); next(); }; /** * ETag generation for GET requests * SAFEGUARD: Checks headersSent before setting headers */ const generateETag = (req, res, next) => { if (req.method !== "GET") { return next(); } const originalJson = res.json.bind(res); res.json = function (data) { try { // SAFEGUARD: Don't process if headers already sent if (res.headersSent) { return originalJson(data); } // Generate simple ETag from stringified data const dataStr = JSON.stringify(data); const etag = `W/"${Buffer.from(dataStr).length.toString(16)}"`; // Check if client has cached version if (req.headers["if-none-match"] === etag) { res.status(304).end(); return; } res.set("ETag", etag); return originalJson(data); } catch (error) { logger.error("ETag generation error", { error: error.message }); return originalJson(data); } }; next(); }; /** * JSON response size optimization * Removes null values and compacts responses */ const optimizeJSON = (req, res, next) => { const originalJson = res.json.bind(res); res.json = function (data) { if (data && typeof data === "object") { data = removeNulls(data); } return originalJson(data); }; next(); }; function removeNulls(obj) { if (Array.isArray(obj)) { return obj.map(removeNulls); } if (obj !== null && typeof obj === "object") { return Object.entries(obj).reduce((acc, [key, value]) => { if (value !== null && value !== undefined) { acc[key] = removeNulls(value); } return acc; }, {}); } return obj; } /** * Batch request handler * Allows multiple API calls in a single request * POST /api/batch with body: { requests: [{ method, url, body }] } */ const batchHandler = async (req, res) => { const { requests } = req.body; if (!Array.isArray(requests) || requests.length === 0) { return res.status(400).json({ success: false, error: "Invalid batch request format", }); } if (requests.length > 10) { return res.status(400).json({ success: false, error: "Maximum 10 requests per batch", }); } const results = await Promise.allSettled( requests.map(async (request) => { try { // This would require implementation of internal request handling // For now, return a placeholder return { status: 200, data: { message: "Batch processing not fully implemented" }, }; } catch (error) { return { status: 500, error: error.message, }; } }) ); res.json({ success: true, results: results.map((result, index) => ({ ...requests[index], ...result, })), }); }; module.exports = { enableCompression, addCacheHeaders, fieldFilter, paginate, trackResponseTime, generateETag, optimizeJSON, batchHandler, };