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 apiforgeRun this from your project root. The CLI will:
- Detect your architecture (monolithic / modular-monolithic / microservices)
- Find your server entry file (or ask)
- Inject a temporary analysis snippet
- Start your server with the command you choose
- Collect all registered routes
- Upload to your APIForge dashboard for review
- 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-analyzerExpress
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
