diff --git a/middleware/src/compression/compression.config.ts b/middleware/src/compression/compression.config.ts new file mode 100644 index 00000000..8dfa33c4 --- /dev/null +++ b/middleware/src/compression/compression.config.ts @@ -0,0 +1,20 @@ +export const COMPRESSION_CONFIG = { + threshold: 1024, // minimum size in bytes + gzip: { level: 6 }, // balance speed vs size + brotli: { quality: 4 }, // modern browsers + skipTypes: [ + /^image\//, + /^video\//, + /^audio\//, + /^application\/zip/, + /^application\/gzip/, + ], + compressibleTypes: [ + 'application/json', + 'text/html', + 'text/plain', + 'application/javascript', + 'text/css', + 'text/xml', + ], +}; diff --git a/middleware/src/compression/compression.middleware.ts b/middleware/src/compression/compression.middleware.ts new file mode 100644 index 00000000..c2c50812 --- /dev/null +++ b/middleware/src/compression/compression.middleware.ts @@ -0,0 +1,63 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import * as zlib from 'zlib'; +import { COMPRESSION_CONFIG } from './compression.config'; + +@Injectable() +export class CompressionMiddleware implements NestMiddleware { + use(req: Request, res: Response, next: NextFunction) { + const acceptEncoding = req.headers['accept-encoding'] || ''; + const chunks: Buffer[] = []; + const originalWrite = res.write; + const originalEnd = res.end; + + // Intercept response body + res.write = function (chunk: any) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + return true; + }; + + res.end = function (chunk: any) { + if (chunk) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + + const body = Buffer.concat(chunks); + + // Skip compression if too small + if (body.length < COMPRESSION_CONFIG.threshold) { + return originalEnd.call(res, body); + } + + const contentType = res.getHeader('Content-Type') as string; + if (COMPRESSION_CONFIG.skipTypes.some((regex) => regex.test(contentType))) { + return originalEnd.call(res, body); + } + + // Select algorithm + if (/\bbr\b/.test(acceptEncoding)) { + zlib.brotliCompress(body, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: COMPRESSION_CONFIG.brotli.quality } }, (err, compressed) => { + if (err) return originalEnd.call(res, body); + res.setHeader('Content-Encoding', 'br'); + originalEnd.call(res, compressed); + }); + } else if (/\bgzip\b/.test(acceptEncoding)) { + zlib.gzip(body, { level: COMPRESSION_CONFIG.gzip.level }, (err, compressed) => { + if (err) return originalEnd.call(res, body); + res.setHeader('Content-Encoding', 'gzip'); + originalEnd.call(res, compressed); + }); + } else if (/\bdeflate\b/.test(acceptEncoding)) { + zlib.deflate(body, (err, compressed) => { + if (err) return originalEnd.call(res, body); + res.setHeader('Content-Encoding', 'deflate'); + originalEnd.call(res, compressed); + }); + } else { + return originalEnd.call(res, body); + } + }; + + next(); + } +}