ReferenceRequest & Body
@nextrush/compression

Compression

Multi-runtime response compression with Gzip, Deflate, and Brotli support.

Problem

Source & internals

Uncompressed HTTP responses waste bandwidth and slow page loads. Text-based payloads (JSON, HTML, CSS) often shrink 60–80% with compression, yet getting it right involves content negotiation, content-type filtering, threshold tuning, and runtime compatibility — details you should not have to manage per-route.

Default Behavior

With default options, the middleware:

  • Enables Gzip, Deflate, and Brotli (where the runtime supports it)
  • Compresses responses larger than 1024 bytes (1 KB)
  • Selects the best encoding from the client's Accept-Encoding header
  • Skips already-compressed content types (images, video, archives)
  • Skips HEAD requests and 204/304 status codes
  • Adds Vary: Accept-Encoding for correct caching
  • Falls back to uncompressed responses on compression failure

Installation

$ pnpm add @nextrush/compression

Minimal Usage

import { createApp } from '@nextrush/core';
import { compression } from '@nextrush/compression';

const app = createApp();
app.use(compression());

Configuration Options

Pass an options object to compression() to override defaults.

CPU Trade-off

Higher compression levels reduce payload size but increase CPU usage per response. Level 6 (default) balances size and speed for most workloads. Use level 1–3 for latency-sensitive APIs or level 9+ only for static pre-compressed assets.

app.use(
  compression({
    gzip: true,
    deflate: true,
    brotli: true,
    level: 6,
    threshold: 1024,
    breachMitigation: false,
  })
);

CompressionOptions

PropertyTypeDescription
gzip?boolean= trueEnable Gzip compression.
deflate?boolean= trueEnable Deflate compression.
brotli?boolean= trueEnable Brotli compression. Disabled automatically on runtimes with no Brotli support at all (Deno, Edge) — but NOT reliably on Bun, see the Runtime Support warning below.
level?number= 6Compression level. 0–9 for Gzip/Deflate, 0–11 for Brotli. Higher values produce smaller output but use more CPU.
threshold?number= 1024Minimum response size in bytes to compress. Responses below this size are sent uncompressed.
contentTypes?readonly string[]= DEFAULT_COMPRESSIBLE_TYPESContent types to compress. Supports exact matches and wildcard patterns (e.g., text/*).
exclude?readonly string[]= DEFAULT_EXCLUDED_TYPESContent types to exclude from compression. Supports exact matches and wildcard patterns.
filter?(ctx: Context) => booleanCustom filter function. Return true to compress, false to skip.
breachMitigation?boolean= falseAdd random padding to prevent BREACH compression ratio analysis attacks.

Algorithm-Specific Middleware

Use dedicated functions when you need a single compression algorithm:

import { gzip, deflate, brotli } from '@nextrush/compression';

app.use(gzip({ level: 6 })); // Gzip only — universal support
app.use(deflate({ level: 6 })); // Deflate only — fast compression
app.use(brotli({ level: 4 })); // Brotli only — best ratio (Node.js; see Runtime Support for Bun)

These accept the same options as compression(), minus the gzip, deflate, and brotli toggles.

Content Negotiation

The middleware parses the Accept-Encoding header and selects the best encoding the server supports. Priority order when quality values are equal:

  1. Brotli (br) — best compression ratio
  2. Gzip (gzip) — widest client support
  3. Deflate (deflate) — fallback
// Client: Accept-Encoding: gzip, br;q=0.9
// Server responds with Gzip (higher quality value)
app.use(compression());

BREACH Attack Mitigation

Responses containing secrets alongside user-controlled input are vulnerable to BREACH attacks. Enable random padding to mitigate:

import { compression, secureCompressionOptions } from '@nextrush/compression';

app.use(compression(secureCompressionOptions()));

// With additional overrides
app.use(compression(secureCompressionOptions({ threshold: 512 })));

secureCompressionOptions() returns a CompressionOptions object with breachMitigation: true and a default level of 4.

Integration Example

Track compression statistics in downstream middleware:

import { compression, getCompressionInfo, wasCompressed } from '@nextrush/compression';

app.use(compression());

app.use(async (ctx) => {
  await ctx.next();

  if (wasCompressed(ctx)) {
    const info = getCompressionInfo(ctx);
    console.log(`${info?.encoding}: ${info?.originalSize} → ${info?.compressedSize} bytes`);
  }
});

Runtime Support

RuntimeGzipDeflateBrotli
Node.js
Bun⚠️ Reports supported, fails closed
Deno
Edge

Brotli is broken on Bun, not merely unsupported

detectCapabilities() hardcodes hasBrotli: true whenever it detects Bun, but never verifies that Bun's CompressionStream actually implements the br format — and Bun has no node:zlib fallback either. If a client's Accept-Encoding negotiates Brotli on Bun, compress() throws internally; the middleware catches it, degrades to sending the response uncompressed, and records the failure on ctx.state.compressionError. Disable Brotli explicitly on Bun (compression({ brotli: false })) — gzip and deflate work correctly there.

Common Mistakes

Setting threshold too low. Compressing responses under ~150 bytes often produces output larger than the original. Keep the default of 1024 bytes unless you have measured a benefit.

Compressing already-compressed content. The middleware skips excluded types by default (PNG, JPEG, ZIP, etc.). If you override contentTypes, make sure you do not include formats that are already compressed.

Using high Brotli levels for dynamic content. Brotli levels 10–11 are designed for offline pre-compression. For dynamic responses, level 4–6 provides a good speed/ratio balance.

Troubleshooting

Responses are not compressed. Verify that the response body exceeds the threshold, the Content-Type matches an entry in contentTypes, and the client sends an Accept-Encoding header.

Double compression. If Content-Encoding is already set on the response, the middleware skips compression. Check that no upstream middleware is adding this header.

Brotli not activating (Node.js/other runtimes). Brotli requires node:zlib or a CompressionStream implementation that supports br. Call detectCapabilities() to inspect what the current runtime reports.

On Bun, Brotli negotiates but the response comes back uncompressed. This is a known runtime gap, not a misconfiguration — see the callout above. Set compression({ brotli: false }) on Bun, or check ctx.state.compressionError to confirm.

Was this helpful?

On this page