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

apiforge-analyzer

v1.3.6

Published

Universal API documentation generator for Express, Next.js, Vite, React Router, and Node.js

Downloads

201

Readme

apiforge-analyzer

CLI and library for auto-generating API documentation from live Express, Fastify, NestJS, Next.js, Vite, and React Router apps.

Routes are extracted at runtime by walking the framework's internal route stack — no static analysis, no decorators, no changes to your production code.

CLI usage

npx apiforge

Run this from your project root. The CLI will:

  1. Detect your architecture (monolithic / modular-monolithic / microservices)
  2. Find your server entry file (or ask)
  3. Inject a temporary analysis snippet
  4. Start your server with the command you choose
  5. Collect all registered routes
  6. Upload to your APIForge dashboard for review
  7. Remove the injected code

For microservices, if an apiforge.config.json exists the CLI iterates every service sequentially, collects routes from each, then uploads everything in one batch. If no config file exists, the CLI scans your services/ directory, shows what it found, and writes the config file for you — so the next run is fully automatic.

// apiforge.config.json — auto-created on first run, commit this file
{
  "projectName": "My Backend",
  "architectureType": "microservices",
  "services": [
    { "name": "auth-service",  "dir": "./services/auth-service" },
    { "name": "user-service",  "dir": "./services/user-service" },
    { "name": "order-service", "dir": "./services/order-service" }
  ]
}

Each service entry can also include "startCommand" to override the default (node <entryFile>):

{ "name": "auth-service", "dir": "./services/auth", "startCommand": "npx tsx services/auth/src/index.ts" }

Programmatic API

Install:

npm install apiforge-analyzer

Express

const express = require('express')
const { analyze } = require('apiforge-analyzer')

const app = express()
app.use(express.json())

app.get('/api/users', getUsers)
app.post('/api/users', createUser)
app.get('/api/users/:id', getUserById)

// Call after all routes are registered
analyze(app, {
  projectName: 'My API',
  apiKey: process.env.APIFORGE_API_KEY,
  baseUrl: 'https://api.myapp.com',
  upload: true,
})

app.listen(3000)

NestJS

import { analyze } from 'apiforge-analyzer'

const app = await NestFactory.create(AppModule)
await app.init()

await analyze(app.getHttpServer(), {
  projectName: 'My NestJS API',
  apiKey: process.env.APIFORGE_API_KEY,
  upload: true,
})

await app.listen(3000)

Fastify

Routes are captured via Fastify's onRoute hook, so composed paths (register(fn, { prefix }), relative registrations, nested scopes) resolve to their real runtime URL rather than the literal string passed to app.get(...). Handlers are resolved with a TypeScript checker (not a regex), so a handler that lives in another module — including one reached through an aliased import, a curried factory, or a thin delegating wrapper — is still followed to its real implementation.

const fastify = require('fastify')
const { analyze } = require('apiforge-analyzer')

const app = fastify()

app.register(async (v1) => {
  v1.get('/users', listUsers)
  v1.post('/users', createUser)
}, { prefix: '/v1' })

// Call after all routes are registered, before or after app.listen — analyze()
// awaits app.ready() itself.
await analyze(app, {
  projectName: 'My API',
  apiKey: process.env.APIFORGE_API_KEY,
  upload: true,
})

await app.listen({ port: 3000 })

What's inferred automatically, with no annotation: the composed path, the handler's resolved source location and leading JSDoc comment (lifted verbatim into description), header reads, literal and simple-ternary response statuses, and auth — a route counts as authenticated if a route-level onRequest/preHandler hook looks like an auth check by name or body, or if the resolved handler calls a guard function (requireScope, requireOperator, or a local alias of either — const operator = requireOperator is followed, not missed).

Query and body fields backed by a zod schema — matching both schema.parse(request.query) and a wrapper call like parse(schema, request.query) — come out typed, not as bare field names: z.string().min/max/length(), .number(), .boolean(), .enum([...]) and .default() are read off the schema and land in the standard Route.parameters / Route.requestBody fields as real JSON Schema (minLength/maxLength, enum, default, etc.), the same fields uploadRoutes() and exportToJSON() read — not a side channel that gets dropped on the way out. A .refine() on the object doesn't drop the fields declared before it.

What can't be inferred safely is reported as "unknown" rather than guessed — an unresolved handler, or a route-level hook whose name doesn't match a known auth pattern, produces authRequired: "unknown" instead of a confident false.

Supplying what can't be inferred

Four extra AnalyzerOptions fields cover metadata no static analysis can recover:

await analyze(app, {
  projectName: 'My API',
  apiKey: process.env.APIFORGE_API_KEY,

  // scope name -> extra metadata, joined onto each route's resolved auth scope
  scopeCatalogPath: './scope-catalog.json',

  // error code -> { status, retry, message, hint? }, joined onto a route by
  // response status, or by the `errors` list in an annotation (see below)
  errorCatalogPath: './error-catalog.json',

  // per-prefix response envelope shape — required for every versioned prefix
  // your API serves once you use this option at all; an unlisted prefix is
  // reported as a warning rather than silently assumed bare
  envelopes: {
    '/v1': { bare: true },
    '/v2': { wrapper: 'ApiEnvelope', dataField: 'data' },
  },
  // With `envelopes` set, `GET /v1/things` and `GET /v2/things` get *different*
  // `responses['200']` schemas from this one declaration — `/v2`'s nests the same
  // bare shape under `data` instead of replacing it, so it stays a strict superset.

  // "METHOD /path" -> { summary?, description?, tags?, deprecated?, errors? }
  // overrides for whatever the analyzer got wrong or couldn't see at all
  annotationsPath: './annotations.json',
})

The same per-route overrides can also be attached inline at the route, via Fastify's own config option — no sidecar file needed for a one-off:

app.get('/users', {
  config: {
    apiforge: { summary: 'List users', tags: ['users'] },
  },
}, listUsers)

An annotationsPath entry that doesn't match any route Fastify actually registered (a stale key left behind after a route was renamed or removed) logs a warning rather than failing silently.

We deliberately do not read Fastify's request-side schema option for documentation — adding a schema there gives the route a second, independent validator with its own error contract, and fast-json-stringify silently drops response fields a response schema doesn't declare. Use zod (auto-detected) or the annotation options above instead.

Microservices — per-service

When running each service independently, pass service to tag routes with the service name:

const { analyze } = require('apiforge-analyzer')

analyze(app, {
  projectName: 'My Platform',
  apiKey: process.env.APIFORGE_API_KEY,
  architectureType: 'microservices',
  service: 'auth-service',
  upload: true,
})

Next.js Pages Router

import { analyzeNextPages } from 'apiforge-analyzer'

await analyzeNextPages({
  projectName: 'My Next.js API',
  apiKey: process.env.APIFORGE_API_KEY,
  nextDir: 'pages/api',
  upload: true,
})

Next.js App Router

import { analyzeNextApp } from 'apiforge-analyzer'

await analyzeNextApp({
  projectName: 'My Next.js API',
  apiKey: process.env.APIFORGE_API_KEY,
  nextDir: 'app',
  upload: true,
})

Vite

import { analyzeVite } from 'apiforge-analyzer'

await analyzeVite({
  projectName: 'My App',
  apiKey: process.env.APIFORGE_API_KEY,
  nextDir: 'vite.config.ts',
  upload: true,
})

React Router

import { analyzeReactRouter } from 'apiforge-analyzer'

await analyzeReactRouter({
  projectName: 'My App',
  apiKey: process.env.APIFORGE_API_KEY,
  nextDir: 'src/routes.tsx',
  upload: true,
})

Options

interface AnalyzerOptions {
  projectName: string          // required

  apiKey?: string              // APIForge API key — also read from APIFORGE_API_KEY env var
  baseUrl?: string             // base URL shown in generated docs (e.g. https://api.myapp.com)
  apiForgeUrl?: string         // override backend URL (default: https://apiforgeapi.brainfogagency.com)

  upload?: boolean             // upload to dashboard (default: true when apiKey is set)
  saveJson?: boolean           // write routes to a local JSON file
  jsonPath?: string            // path for the JSON file (default: api-export.json)

  architectureType?: 'monolithic' | 'modular-monolithic' | 'microservices'
  service?: string             // service name for microservices mode — stamped on every route

  framework?: string           // skip auto-detection and force a framework
  nextDir?: string             // directory or config file path for Next.js / Vite / React Router

  skipInProduction?: boolean   // skip when NODE_ENV=production (default: true)
  runOnce?: boolean            // debounce — skip if analyzed within the last 30 s (default: true)

  // Fastify only — see "Supplying what can't be inferred" above
  scopeCatalogPath?: string
  errorCatalogPath?: string
  envelopes?: Record<string, { wrapper: string; dataField: string; excluded?: string[] } | { bare: true }>
  annotationsPath?: string
}

analyze() throws instead of resolving to [] when the detected or forced framework has no analyzer (currently: anything other than express, fastify, nestjs, nextjs-pages, nextjs-app, vite, react-router). Every other early-exit (no API key, debounced, zero routes found, NODE_ENV=production) still resolves to [] — only "this framework cannot be analyzed" is a thrown error, so a caller can tell the two apart. detectFramework() also exposes this directly: detectFramework(app).supported is false for a framework it can name but not analyze (currently koa, hapi, unknown).

Architecture detection

The CLI scores structural signals to pick the right architecture automatically:

| Signal | Architecture | |---|---| | apiforge.config.json with services[] | microservices | | docker-compose.yml with 3+ node services | microservices | | Multiple package.json files in sibling dirs | microservices | | src/modules/ or src/features/ directory | modular-monolithic | | *.module.ts files (NestJS @Module) | modular-monolithic | | None of the above | monolithic |

For modular-monolithic projects, routes are automatically grouped by module using path-prefix matching — /api/users/* gets tagged as the users module.

Output format

Each collected route:

{
  method: 'GET',
  path: '/api/users/:id',
  tag: 'users',
  operationId: 'get-api-users-id',
  summary: 'Get api users',
  middleware: ['authenticate'],
  authRequired: true,
  pathParams: ['id'],
  queryParams: [],
  requestBodyFields: [],
  responseStatuses: [200, 404],
  framework: 'express',
  module?: 'users',        // modular-monolithic only
  service?: 'auth-service' // microservices only
}

Escape hatch — building routes yourself

If your framework has no analyze*() function, call uploadRoutes() directly with a hand-built Route[]. This is the same function every analyze*() call ends up using internally — it's a supported, typed, top-level export, not a private helper.

import { uploadRoutes, type Route } from 'apiforge-analyzer'

const routes: Route[] = [
  {
    method: 'GET',
    path: '/api/users/:id',
    tag: 'users',
    operationId: 'get_api_users_id',
    summary: 'Get a user by id',
    middleware: [],
    authRequired: true,
    pathParams: ['id'],
    queryParams: [],
    requestBodyFields: [],
    linkedPaths: [],
    hasAuth: true,
    framework: 'my-framework',
    // routeKey?: stable dedup key the dashboard uses across uploads — see below
  },
]

await uploadRoutes(routes, {
  projectName: 'My API',
  apiKey: process.env.APIFORGE_API_KEY!,
})

The fields above (method, path, tag, operationId, summary, middleware, authRequired, pathParams, queryParams, requestBodyFields, linkedPaths, hasAuth, framework) are the only ones Route requires; everything else (description, tags, headerParams, responseStatuses, parameters, requestBody, responses, module, service, ...) is optional and gets a reasonable default from enrichRoutes().

Set routeKey yourself. If you don't, it defaults to api-001, api-002, ... based on array position — which shifts if a route is added, removed, or reordered between uploads, so the dashboard can no longer tell "this is the same endpoint as last time" from "this is a new one". Use something stable per endpoint, e.g. `${method} ${path}`, as every built-in analyzer does.

ESM and CommonJS

// ESM
import { analyze } from 'apiforge-analyzer'

// CommonJS
const { analyze } = require('apiforge-analyzer')

Environment variables

| Variable | Description | |---|---| | APIFORGE_API_KEY | API key — loaded automatically, no dotenv required | | APIFORGE_URL | Override backend URL | | NODE_ENV | Set to production to disable analysis |

Get an API key

apiforge.brainfogagency.com/dashboard

License

MIT