npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

api-observatory

v1.1.1

Published

API Traffic Metrics & Latency Percentiles — zero-dependency core with Express, Fastify, and Koa adapters

Readme


Add two lines of code. Get a full metrics dashboard, auto-generated API docs, and an interactive guide — all served from your running app at /_observatory.

Table of Contents

Why I Built This

Every API starts the same way: you build endpoints, ship them, and assume they're fast enough. Then one day a user reports that "the app feels slow." You open your terminal, realize you have no idea which endpoint is the bottleneck, and start googling how to set up Prometheus with Grafana. An hour later you're writing YAML files and configuring dashboards instead of fixing the actual problem.

I wanted something I could drop into any Express/Fastify/Koa app and immediately see which endpoints are slow, what the P95 looks like, and whether error rates are climbing — without installing a single dependency, without configuring an external service, without leaving my app. Two lines of code, open a browser tab, done.

The API docs feature came next. I was tired of maintaining OpenAPI specs that drifted from reality within a week. If the middleware is already watching every request and response, why not infer the schemas automatically? So it does — you get Swagger-like docs generated from your live traffic, always accurate, always current.

What This Is (and Isn’t)

This is not a replacement for full observability stacks. It’s a lightweight, in-app way to answer:

  • Which endpoints are slow?
  • How bad is the tail latency?
  • Are errors creeping up?

If you need distributed tracing across 20 services, use the big tools. If you want answers in 30 seconds, use this.

What You Get

Metrics Dashboard

See every endpoint's request count, P50/P95/P99 latency, error rates, and throughput at a glance. Latency values are color-coded (green/yellow/red) so you can spot slow endpoints instantly.

Auto-Generated API Docs

Enable schema capture and API Observatory builds Swagger-like documentation from your live traffic — no manual spec writing needed. Endpoints are grouped by path prefix, path parameters are highlighted, and request/response schemas are inferred automatically.

Interactive Guide

A built-in reference explaining what each metric means, how to act on them, and API design best practices — right inside your dashboard.

Why API Observatory?

| | | | --------------------- | ------------------------------------------------------------------------------ | | Zero dependencies | No runtime dependencies. Nothing to audit, nothing to break. | | Zero config | Works out of the box with sensible defaults. | | Off the hot path | Metrics are recorded via setImmediate() — your response times stay the same. | | Fixed memory | Circular buffer storage with automatic eviction. No memory leaks. | | Three frameworks | Express, Fastify, and Koa adapters with identical feature sets. | | Schema inference | Auto-generates API docs from live traffic — no OpenAPI spec required. |

Quick Start

npm install api-observatory

Express

import express from "express";
import { expressObservatory } from "api-observatory";

const app = express();
app.use(expressObservatory());
app.use(express.json());
// ... your routes
app.listen(3000);

Note: If you enable auth (login-based authentication), mount express.json() and express.urlencoded() before expressObservatory() so the login form body is parsed. See Login-Based Authentication.

Fastify

import Fastify from "fastify";
import { fastifyObservatory } from "api-observatory";

const app = Fastify();
app.register(fastifyObservatory);
// ... your routes
app.listen({ port: 3000 });

Koa

import Koa from "koa";
import { koaObservatory } from "api-observatory";

const app = new Koa();
app.use(koaObservatory()); // Mount FIRST
// ... your routes
app.listen(3000);

Then open http://localhost:3000/_observatory in your browser.

Schema Capture (API Docs)

Schema capture intercepts request and response bodies to build inferred JSON schemas from live traffic. Enable it to get the API Docs tab.

In code:

app.use(expressObservatory({ captureSchemas: true }));

Or via environment variable:

OBSERVATORY_CAPTURE_SCHEMAS=true

How it works

  1. Request bodies are captured from req.body (requires a body parser)
  2. Response bodies are captured by intercepting res.json() / onSend hook / ctx.body
  3. Schemas are inferred recursively from each observed body
  4. Repeated observations are merged — fields present in every request are marked required, others optional
  5. Type conflicts are widened (e.g., string | number)

All schema processing runs off the hot path via setImmediate(). It does not block responses. For high-throughput production APIs with large payloads, consider enabling it only in staging/development.

Important: Mount the middleware once. Multiple instances have isolated stores, so data will be split and the dashboard will only show partial data.

Configuration

All options are optional.

app.use(expressObservatory({
  mountPath: '/_observatory',        // Dashboard URL path
  includePaths: ['/api/**'],         // Only track these routes (glob)
  excludePaths: ['/health'],         // Skip these routes (glob)
  retentionMs: 3_600_000,           // 1 hour retention window
  maxPerEndpoint: 10_000,           // Circular buffer capacity
  percentiles: [50, 95, 99],        // Which percentiles to compute
  htmlDashboard: true,              // Serve HTML (false = JSON only)
  captureSchemas: true,             // Enable schema inference
  apiKey: 'your-secret-key',        // Dashboard authentication
  redactFields: true,               // Redact sensitive field names
  rateLimit: { windowMs: 60000, maxRequests: 100 },
  onRecord: (record) => { ... },    // Forward to external systems
  onDashboardAccess: (event) => { ... }, // Audit logging
}));

| Option | Type | Default | Description | | ------------------- | --------------------- | ------------------ | -------------------------------------- | | mountPath | string | /_observatory | Dashboard URL path | | includePaths | string[] | [] | Glob patterns to track (empty = all) | | excludePaths | string[] | [mountPath, ...] | Glob patterns to skip | | retentionMs | number | 3_600_000 | Metrics retention window (ms) | | maxPerEndpoint | number | 10_000 | Max records per endpoint | | percentiles | number[] | [50, 95, 99] | Percentiles to compute | | htmlDashboard | boolean | true | Serve HTML dashboard | | captureSchemas | boolean | false | Enable schema capture | | apiKey | string | — | API key for dashboard authentication | | authenticate | function | — | Custom auth (headers) => boolean | | redactFields | boolean \| string[] | false | Redact sensitive field names in schemas| | rateLimit | object | — | { windowMs, maxRequests } for rate limiting | | onRecord | function | — | Callback after each record | | onDashboardAccess | function | — | Audit callback for dashboard access | | auth | object | — | Login-based auth { users, verify, sessionSecret } |

| Environment Variable | Description | | ----------------------------- | ----------------------------------------------------------- | | OBSERVATORY_CAPTURE_SCHEMAS | Set to true to enable schema capture without code changes | | OBSERVATORY_API_KEY | Set API key for dashboard authentication |

Security

By default, the dashboard is accessible to anyone with network access to your application. For production deployments, always enable security features.

Authentication

Why: Without authentication, anyone with network access can view your API metrics, inferred schemas, and reset your data. In production, this exposes sensitive information about your API structure and usage patterns.

API Key Authentication — The simplest way to protect the dashboard:

app.use(expressObservatory({
  apiKey: process.env.OBSERVATORY_API_KEY
}));

Access the dashboard with the X-Observatory-Key header:

curl -H "X-Observatory-Key: your-secret-key" http://localhost:3000/_observatory/metrics

Custom Authentication — For JWT, session-based, or other complex auth:

app.use(expressObservatory({
  authenticate: (headers) => {
    const auth = headers['authorization'];
    if (!auth || !auth.startsWith('Bearer ')) return false;
    const token = auth.slice(7);
    return isValidToken(token); // Your validation logic
  }
}));

Login-Based Authentication

For teams who need a proper login experience with username/password, use the auth option. Users are presented with a built-in login page:

Important (Express): When using auth, mount body parsers before the observatory middleware so that req.body is available for the login form:

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(expressObservatory({ auth: { ... } }));

Config-based users — Define users directly in your config:

app.use(expressObservatory({
  auth: {
    users: [
      { username: 'alice', password: process.env.ALICE_PW },
      { username: 'bob', password: process.env.BOB_PW },
    ],
    sessionSecret: process.env.SESSION_SECRET,  // Required - for signing cookies
    sessionMaxAge: 86400_000,  // Optional - 24 hours default
  }
}));

Custom verify function — Integrate with your own database, LDAP, or SSO:

app.use(expressObservatory({
  auth: {
    verify: async (username, password) => {
      const user = await db.users.findOne({ username });
      if (!user) return null;

      const valid = await bcrypt.compare(password, user.passwordHash);
      return valid ? { username: user.username, email: user.email } : null;
    },
    sessionSecret: process.env.SESSION_SECRET,
  }
}));

How it works:

  1. Users visit /_observatory and are redirected to /_observatory/login
  2. After successful login, a signed session cookie is set
  3. Users can access the dashboard until the session expires
  4. Logout is available at /_observatory/logout

Login endpoints:

| Path | Method | Description | |------|--------|-------------| | /_observatory/login | GET | Login page | | /_observatory/login | POST | Verify credentials | | /_observatory/logout | GET | Clear session |

The auth option takes precedence over apiKey and authenticate if multiple are configured.

Field Name Redaction

Why: Schema capture infers field names from request/response bodies. Field names like password, ssn, credit_card, or api_key reveal sensitive data patterns even without exposing values. Attackers can use this information to understand your data model and target specific endpoints.

Solution: The redactFields option automatically redacts sensitive field names from schema output. Fields are replaced with { type: '[REDACTED]' }.

app.use(expressObservatory({
  captureSchemas: true,
  redactFields: true,  // Use default sensitive fields list
}));

Default redacted fields: password, passwd, secret, token, api_key, apikey, access_token, refresh_token, authorization, credential, private_key, ssn, credit_card, cvv, pin

Custom redaction list:

app.use(expressObservatory({
  captureSchemas: true,
  redactFields: ['password', 'ssn', 'secret', 'custom_sensitive_field'],
}));

Rate Limiting

Why: Dashboard endpoints can be abused for reconnaissance or denial-of-service attacks. Without rate limiting, attackers can rapidly probe your API structure or overwhelm the metrics system.

Solution: The rateLimit option applies a sliding-window rate limit to all dashboard endpoints.

app.use(expressObservatory({
  rateLimit: {
    windowMs: 60_000,   // 1 minute window
    maxRequests: 100,   // Max 100 requests per window
  }
}));

Rate limit is applied per API key (if authenticated) or per IP address. When exceeded, the dashboard returns 429 Too Many Requests with a retryAfterMs field.

Audit Logging

Why: In production, you need visibility into who accesses your metrics dashboard, when, and what they viewed. This is critical for security audits, compliance, and detecting unauthorized access.

Solution: The onDashboardAccess callback fires on every dashboard access attempt, successful or not.

app.use(expressObservatory({
  onDashboardAccess: (event) => {
    console.log({
      type: event.type,           // 'access_granted' | 'access_denied' | 'metrics_viewed' | 'schemas_viewed' | 'reset'
      timestamp: event.timestamp,
      endpoint: event.endpoint,
      method: event.method,
      ip: event.ip,
    });
    // Forward to your logging system (Datadog, Splunk, CloudWatch, etc.)
  }
}));

Event types:

  • access_granted — Authentication passed
  • access_denied — Authentication failed or rate limited
  • metrics_viewed — Metrics endpoint accessed
  • schemas_viewed — Schema endpoint accessed
  • reset — Metrics/schemas were reset
  • login — User logged in successfully
  • logout — User logged out

Security Options Reference

| Option | Type | Default | Description | | ------------------- | ----------------------- | ------- | --------------------------------------------------- | | apiKey | string | — | Require X-Observatory-Key header with this value | | authenticate | (headers) => boolean | — | Custom auth function | | auth | object | — | Login-based auth { users, verify, sessionSecret } | | redactFields | boolean \| string[] | false | Redact sensitive field names in schemas | | rateLimit | object | — | Rate limit config { windowMs, maxRequests } | | onDashboardAccess | (event) => void | — | Audit callback for all dashboard access |

Production Recommendations

// Recommended production config
app.use(expressObservatory({
  // Authentication
  apiKey: process.env.OBSERVATORY_API_KEY,

  // Rate limiting
  rateLimit: {
    windowMs: 60_000,
    maxRequests: 100,
  },

  // Schema security
  captureSchemas: true,          // Enable if needed
  redactFields: true,            // Redact sensitive fields

  // Audit logging
  onDashboardAccess: (event) => {
    logger.info('Observatory access', event);
  },

  // Exclude sensitive routes
  excludePaths: ['/auth/**', '/admin/**'],
}));

Checklist:

  1. Enable authentication — Use apiKey, authenticate, or auth (login-based)
  2. Enable redaction — Use redactFields: true if using schema capture
  3. Enable rate limiting — Prevent abuse with rateLimit
  4. Enable audit logging — Track access with onDashboardAccess
  5. Use HTTPS — Protect API keys and session cookies in transit
  6. Restrict network access — Firewall/VPC rules where possible

API Endpoints

| Method | Path | Description | | ------ | --------------------------------- | -------------------------------------------------- | | GET | /_observatory | HTML dashboard (or JSON if htmlDashboard: false) | | GET | /_observatory/metrics | All endpoint metrics as JSON | | GET | /_observatory/metrics/:method/* | Single endpoint metrics | | GET | /_observatory/schemas | All captured schemas as JSON | | GET | /_observatory/schemas/:method/* | Single endpoint schema | | POST | /_observatory/reset | Clear all metrics and schemas |

Route Extraction

API Observatory automatically extracts parameterized route patterns (e.g., /v1/users/:id) instead of raw URLs. When route info is unavailable, it normalizes paths by replacing UUIDs, ObjectIds, and numeric IDs with :id.

| Framework | Source | | --------- | ------------------------------ | | Express | req.baseUrl + req.route.path | | Fastify | request.routeOptions.url | | Koa | ctx._matchedRoute |

Requirements

  • Node.js >= 18
  • Express >= 4, Fastify >= 4, or Koa >= 2 (peer dependencies, all optional)

License

MIT