@phil-r/stats
v2.1.0
Published
Request statistics middleware
Maintainers
Readme
stats
Small in-memory request statistics collector for Elysia, Express/Node HTTP, Fastify, Hono and NestJS (Express and Fastify platforms). TypeScript-first, ESM-only, for Node 22+ and Bun. Other frameworks may be integrated through an appropriate adapter.
Installation
npm install @phil-r/stats
# or: bun add @phil-r/statsInstall your framework separately. Framework peers are optional, and the root export loads no framework. No stats endpoint is exposed automatically; protect any endpoint you add.
Usage
Create a collector and install the adapter before routes or middleware that may respond early. Share a collector to aggregate counts across adapters; install each adapter once per request path (Elysia deduplicates by collector).
Elysia
import { Elysia } from 'elysia';
import { createStats } from '@phil-r/stats';
import { elysiaStats } from '@phil-r/stats/elysia';
const stats = createStats({ endpointStats: true, addHeader: true });
new Elysia()
.use(elysiaStats(stats))
.get('/', () => 'Hello')
.get('/stats', () => stats.getStats())
.listen(8080);Express / Node HTTP
import express from 'express';
import { createStats } from '@phil-r/stats';
import { expressStats } from '@phil-r/stats/express';
const stats = createStats();
const app = express();
app.use(expressStats(stats));
app.get('/', (_req, res) => res.send('Hello'));
app.listen(8080);The middleware also accepts Node IncomingMessage/ServerResponse and next,
without importing Express at runtime.
Fastify and Hono
With a collector named stats, register the appropriate adapter:
import { fastifyStats } from '@phil-r/stats/fastify';
// Fastify: native HTTP/1 hook, not a plugin for register().
app.addHook('onRequest', fastifyStats(stats));import { honoStats } from '@phil-r/stats/hono';
// Hono on Node/Bun, not edge runtimes: install first.
app.use('*', honoStats(stats));For NestJS, use its platform's adapter after create, before init/listen.
See Nest examples and integration details or
runnable apps.
Collector API
createStats(options?) returns an independent collector. Features are disabled
by default; paths are unchanged unless normalized or grouped:
endpointStats: per-method/path totals, excluding query strings.endpointNormalizer: synchronous(pathname: string) => string, called only withendpointStatsenabled. Receives the query-free pathname beforecomplexEndpointsmatching and counter allocation; HTTP methods stay separate. Exceptions are not caught, so keep the callback non-throwing.complexEndpoints: ordered regexparam patterns, e.g.['/user/:id']; first match groups dynamic URLs. Use withendpointStatsto limit unique keys.customStats: include named custom measurements in output.addHeader: addX-Response-Time: Nms, preserving existing values.
Normalize noisy bot paths before allocating counters, alongside dynamic routes:
const stats = createStats({
endpointStats: true,
endpointNormalizer: (pathname) =>
pathname.endsWith('.php') ? '/*.php' : pathname,
complexEndpoints: ['/user/:id'],
});stats.getStats() returns request count, total/average milliseconds, status
counts, process identity/uptime and enabled endpoint/custom totals. Nested maps
are live; treat them as read-only. Counters are process-local and unbounded.
const stats = createStats({ customStats: true });
const measurement = stats.startMeasurement('work');
try {
await doWork();
} finally {
stats.finishMeasurement(measurement);
}Finish each token once on its originating collector. Timings are monotonic milliseconds, not body-transmission times. See the guide for precise adapter timing, supported versions, migration and development commands.
License
MIT.
