Expediate ships a suite of production-ready middleware. All factories return standard (req, res, next) functions and can be mounted globally or scoped to specific paths.
Transparent response compression. Must be mounted before any middleware that writes response bodies.
Algorithm priority (based on Accept-Encoding): Brotli > gzip > deflate.
import { compress } from 'expediate';
app.use(compress());
app.use(compress({ threshold: 512, brotliQuality: 6 }));| Option | Type | Default | Description |
|---|---|---|---|
threshold |
number |
1024 |
Minimum response size in bytes before compression is applied |
br |
boolean |
true |
Enable Brotli compression |
brotliQuality |
number |
4 |
Brotli quality level (0–11) |
gzipLevel |
number |
Node default | gzip/deflate compression level (1–9) |
filter |
(req, res) => boolean |
— | Custom predicate; return false to skip compression for a response |
The middleware removes Content-Length (size changes after compression) and sets Content-Encoding and Vary: Accept-Encoding.
Handles If-None-Match and If-Modified-Since headers (RFC 7232). When the client's cached response is still fresh, sends 304 Not Modified with no body instead of the full response.
import { conditionalGet } from 'expediate';
app.get('/users/:id', conditionalGet(), (req, res) => {
const user = getUser(req.params.id);
res.etag(user.updatedAt.toISOString());
res.json(user); // → 304 when the client is up to date
});Freshness checks follow RFC 7232 priority: If-None-Match (weak comparison, * wildcard supported) first, then If-Modified-Since. Only GET and HEAD are eligible — other methods pass through unchanged.
A 304 response strips Content-Type, Content-Length, and Content-Encoding but retains ETag, Cache-Control, Vary, and Last-Modified.
Sets Cache-Control, Expires, and Vary response headers.
import { cacheControl } from 'expediate';
app.use('/api', cacheControl({ noStore: true }));
app.use('/assets', cacheControl({ maxAge: 31_536_000, immutable: true }));| Option | Type | Description |
|---|---|---|
maxAge |
number |
max-age=<seconds>. Also sets Expires |
sMaxAge |
number |
s-maxage=<seconds> for CDN/shared caches |
public |
boolean |
public directive |
private |
boolean |
private directive |
noStore |
boolean |
no-store — disables all caching |
noCache |
boolean |
no-cache — requires revalidation |
mustRevalidate |
boolean |
must-revalidate |
immutable |
boolean |
immutable — response body will never change within max-age |
vary |
string | string[] |
Sets the Vary header |
Attaches a unique ID to every request and echoes it in the response header.
import { requestId } from 'expediate';
app.use(requestId());
app.get('/health', (req, res) => {
res.json({ id: req.id, status: 'ok' });
});| Option | Type | Default | Description |
|---|---|---|---|
header |
string |
'x-request-id' |
Header name to read and echo |
allowFromHeader |
boolean |
true |
Reuse client-supplied ID. Set false to always generate a new one |
generator |
() => string |
crypto.randomUUID |
Custom ID generator |
The ID is exposed as req.id on the augmented request object.
In-memory sliding-window rate limiting. State is per-process and lost on restart.
import { rateLimit } from 'expediate';
// 100 requests per minute per IP (global)
app.use(rateLimit({ windowMs: 60_000, max: 100 }));
// Tighter limit on authentication endpoints
app.post('/auth/login', rateLimit({ windowMs: 60_000, max: 5 }), loginHandler);| Option | Type | Default | Description |
|---|---|---|---|
windowMs |
number |
required | Sliding window duration in milliseconds |
max |
number |
required | Max requests per key within the window |
keyBy |
(req) => string |
req.ip |
Function to extract the rate-limit key |
message |
string |
'Too Many Requests' |
Response body when limit is exceeded |
statusCode |
number |
429 |
HTTP status when limit is exceeded |
headers |
boolean |
true |
Set X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset |
Not suitable for multi-process deployments without a shared external store.
CSRF protection via the double-submit cookie pattern. Safe methods (GET, HEAD, OPTIONS, TRACE) are exempted. State-mutating methods must supply the token in the X-CSRF-Token header or _csrf body field.
import { csrf } from 'expediate';
app.use(csrf());
// Expose the token to client-side JavaScript
app.get('/form', (req, res) =>
res.send(`<input type="hidden" name="_csrf" value="${req.csrfToken!()}">`));
// POST /form — token validated automatically| Option | Type | Default | Description |
|---|---|---|---|
cookieName |
string |
'_csrf' |
Cookie storing the token |
headerName |
string |
'x-csrf-token' |
Request header carrying the token |
fieldName |
string |
'_csrf' |
Parsed body field (fallback when header is absent) |
secure |
boolean |
false |
Mark the CSRF cookie as Secure |
sameSite |
'Strict'|'Lax'|'None' |
'Strict' |
SameSite attribute of the cookie |
The cookie is not HttpOnly — client JavaScript must be able to read it to include it in requests.
Sets a hardened baseline of HTTP security response headers.
import { securityHeaders } from 'expediate';
app.use(securityHeaders());
// Disable HSTS for plain-HTTP development servers
app.use(securityHeaders({ hsts: false }));| Header | Default value |
|---|---|
Strict-Transport-Security |
max-age=15552000; includeSubDomains |
X-Frame-Options |
SAMEORIGIN |
X-Content-Type-Options |
nosniff |
Referrer-Policy |
strict-origin-when-cross-origin |
Permissions-Policy |
geolocation=(), microphone=(), camera=() |
X-XSS-Protection |
0 |
Pass false to disable a header, or a string to override it:
app.use(securityHeaders({
hsts: false,
frameOptions: 'DENY',
permissionsPolicy: "geolocation=(self 'https://maps.example.com')",
}));Adds Cross-Origin Resource Sharing headers. Headers are only set when the request includes an Origin header (browser-only). OPTIONS preflight requests are handled and terminated — next() is not called for preflights.
import { cors } from 'expediate';
app.use(cors({ origin: 'https://example.com' }));
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
allowCredentials: true,
maxAge: 86400,
}));When origin is an array, the middleware compares the request Origin against the list and echoes the matching origin (or omits the header if no match). Vary: Origin is added automatically when origin matching is dynamic.
| Option | Type | Default | Description |
|---|---|---|---|
origin |
string | string[] |
'*' |
Allowed origin(s). Array triggers request-origin matching |
allowHeaders |
string | string[] |
'Accept, Content-Type, Authorization' |
Access-Control-Allow-Headers value |
allowMethods |
string | string[] |
'GET,HEAD,PUT,PATCH,POST,DELETE' |
Access-Control-Allow-Methods value |
allowCredentials |
boolean |
— | Set Access-Control-Allow-Credentials: true |
maxAge |
number |
— | Preflight cache lifetime in seconds |
vary |
string | string[] |
— | Additional Vary header value |
optionsStatus |
number |
204 |
Status code for OPTIONS preflight responses |
preflight |
(req) => boolean |
— | Custom guard; return false to reject (OPTIONS → 403, others → 400) |
Logs one line per completed request. Integrated with the request lifecycle — the timer starts when the request arrives and fires when the response finishes.
import { logger } from 'expediate';
app.use(logger({
user: (req) => (req as any).user?.username ?? '-',
locale: 'en-US',
logger: (msg) => process.stderr.write(msg + '\n'),
}));Default output format (ANSI-coloured by status class):
21 Mar, 14:32 200 GET /api/users 127.0.0.1 alice 4 ms (1234)
With json: true, the logger calls the logger function with a structured object instead of a string:
{
timestamp: '2024-03-21T14:32:00.000Z',
status: 200,
method: 'GET',
path: '/api/users',
ip: '127.0.0.1',
user: 'alice',
elapsed: 4,
host: 'localhost:3000',
length: 256,
}| Option | Type | Default | Description |
|---|---|---|---|
track |
boolean |
false |
Emit a LOST warning for requests that never complete |
trackTimeout |
number |
30000 |
Milliseconds before emitting the LOST warning |
user |
(req) => string |
() => '-' |
Extract a user identity from the request |
locale |
string |
'en-GB' |
BCP 47 locale for the timestamp |
dateFormat |
Intl.DateTimeFormatOptions |
short date+time | Timestamp format options |
json |
boolean |
false |
Log structured objects instead of formatted strings |
logger |
(msg: string | object) => void |
console.log |
Custom logging sink |