apiforge-analyzer 1.3.8 → 1.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +474 -474
- package/dist/cjs/cli.js +38 -2
- package/dist/cjs/detect.js +10 -1
- package/dist/cjs/express-mount-hook.js +82 -0
- package/dist/cjs/express-preload.js +17 -0
- package/dist/cjs/frameworks/express.js +41 -22
- package/dist/cjs/uploader.js +11 -0
- package/dist/esm/cli.js +40 -4
- package/dist/esm/detect.js +10 -1
- package/dist/esm/express-mount-hook.js +77 -0
- package/dist/esm/express-preload.js +15 -0
- package/dist/esm/frameworks/express.js +41 -22
- package/dist/esm/uploader.js +11 -0
- package/dist/types/express-mount-hook.d.ts +19 -0
- package/dist/types/express-preload.d.ts +1 -0
- package/dist/types/uploader.d.ts +6 -0
- package/package.json +66 -66
package/README.md
CHANGED
|
@@ -1,474 +1,474 @@
|
|
|
1
|
-
# apiforge-analyzer
|
|
2
|
-
|
|
3
|
-
CLI and library for auto-generating API documentation from live Express, Fastify, NestJS, Next.js, Vite, and React Router apps — and Python FastAPI, Flask, and Django apps.
|
|
4
|
-
|
|
5
|
-
Routes are extracted at runtime by walking the framework's internal route stack — no static analysis, no decorators, no changes to your production code.
|
|
6
|
-
|
|
7
|
-
## CLI usage
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npx apiforge
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Run this from your project root. The CLI will:
|
|
14
|
-
|
|
15
|
-
1. Detect your architecture (monolithic / modular-monolithic / microservices)
|
|
16
|
-
2. Find your server entry file (or ask)
|
|
17
|
-
3. Inject a temporary analysis snippet
|
|
18
|
-
4. Start your server with the command you choose
|
|
19
|
-
5. Collect all registered routes
|
|
20
|
-
6. Upload to your APIForge dashboard for review
|
|
21
|
-
7. Remove the injected code
|
|
22
|
-
|
|
23
|
-
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.
|
|
24
|
-
|
|
25
|
-
```json
|
|
26
|
-
// apiforge.config.json — auto-created on first run, commit this file
|
|
27
|
-
{
|
|
28
|
-
"projectName": "My Backend",
|
|
29
|
-
"architectureType": "microservices",
|
|
30
|
-
"services": [
|
|
31
|
-
{ "name": "auth-service", "dir": "./services/auth-service" },
|
|
32
|
-
{ "name": "user-service", "dir": "./services/user-service" },
|
|
33
|
-
{ "name": "order-service", "dir": "./services/order-service" }
|
|
34
|
-
]
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Each service entry can also include `"startCommand"` to override the default (`node <entryFile>`):
|
|
39
|
-
|
|
40
|
-
```json
|
|
41
|
-
{ "name": "auth-service", "dir": "./services/auth", "startCommand": "npx tsx services/auth/src/index.ts" }
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Programmatic API
|
|
45
|
-
|
|
46
|
-
Install:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
npm install apiforge-analyzer
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Express
|
|
53
|
-
|
|
54
|
-
```js
|
|
55
|
-
const express = require('express')
|
|
56
|
-
const { analyze } = require('apiforge-analyzer')
|
|
57
|
-
|
|
58
|
-
const app = express()
|
|
59
|
-
app.use(express.json())
|
|
60
|
-
|
|
61
|
-
app.get('/api/users', getUsers)
|
|
62
|
-
app.post('/api/users', createUser)
|
|
63
|
-
app.get('/api/users/:id', getUserById)
|
|
64
|
-
|
|
65
|
-
// Call after all routes are registered
|
|
66
|
-
analyze(app, {
|
|
67
|
-
projectName: 'My API',
|
|
68
|
-
apiKey: process.env.APIFORGE_API_KEY,
|
|
69
|
-
baseUrl: 'https://api.myapp.com',
|
|
70
|
-
upload: true,
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
app.listen(3000)
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
### NestJS
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
import { analyze } from 'apiforge-analyzer'
|
|
80
|
-
|
|
81
|
-
const app = await NestFactory.create(AppModule)
|
|
82
|
-
await app.init()
|
|
83
|
-
|
|
84
|
-
await analyze(app.getHttpServer(), {
|
|
85
|
-
projectName: 'My NestJS API',
|
|
86
|
-
apiKey: process.env.APIFORGE_API_KEY,
|
|
87
|
-
upload: true,
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
await app.listen(3000)
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### Fastify
|
|
94
|
-
|
|
95
|
-
Routes are captured via Fastify's `onRoute` hook, so composed paths (`register(fn, { prefix })`,
|
|
96
|
-
relative registrations, nested scopes) resolve to their real runtime URL rather than the literal
|
|
97
|
-
string passed to `app.get(...)`. Handlers are resolved with a TypeScript checker (not a regex),
|
|
98
|
-
so a handler that lives in another module — including one reached through an aliased import,
|
|
99
|
-
a curried factory, or a thin delegating wrapper — is still followed to its real implementation.
|
|
100
|
-
|
|
101
|
-
```js
|
|
102
|
-
const fastify = require('fastify')
|
|
103
|
-
const { analyze } = require('apiforge-analyzer')
|
|
104
|
-
|
|
105
|
-
const app = fastify()
|
|
106
|
-
|
|
107
|
-
app.register(async (v1) => {
|
|
108
|
-
v1.get('/users', listUsers)
|
|
109
|
-
v1.post('/users', createUser)
|
|
110
|
-
}, { prefix: '/v1' })
|
|
111
|
-
|
|
112
|
-
// Call after all routes are registered, before or after app.listen — analyze()
|
|
113
|
-
// awaits app.ready() itself.
|
|
114
|
-
await analyze(app, {
|
|
115
|
-
projectName: 'My API',
|
|
116
|
-
apiKey: process.env.APIFORGE_API_KEY,
|
|
117
|
-
upload: true,
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
await app.listen({ port: 3000 })
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
What's inferred automatically, with no annotation: the composed path, the handler's resolved
|
|
124
|
-
source location and leading JSDoc comment (lifted verbatim into `description`), header reads,
|
|
125
|
-
literal and simple-ternary response statuses, and auth — a route counts as authenticated if a
|
|
126
|
-
route-level `onRequest`/`preHandler` hook looks like an auth check by name or body, or if the
|
|
127
|
-
resolved handler calls a guard function (`requireScope`, `requireOperator`, or a local alias of
|
|
128
|
-
either — `const operator = requireOperator` is followed, not missed).
|
|
129
|
-
|
|
130
|
-
Query and body fields backed by a zod schema — matching both `schema.parse(request.query)` and
|
|
131
|
-
a wrapper call like `parse(schema, request.query)` — come out typed, not as bare field names:
|
|
132
|
-
`z.string().min/max/length()`, `.number()`, `.boolean()`, `.enum([...])` and `.default()` are
|
|
133
|
-
read off the schema and land in the standard `Route.parameters` / `Route.requestBody` fields as
|
|
134
|
-
real JSON Schema (`minLength`/`maxLength`, `enum`, `default`, etc.), the same fields
|
|
135
|
-
`uploadRoutes()` and `exportToJSON()` read — not a side channel that gets dropped on the way out.
|
|
136
|
-
A `.refine()` on the object doesn't drop the fields declared before it.
|
|
137
|
-
|
|
138
|
-
What can't be inferred safely is reported as `"unknown"` rather than guessed — an unresolved
|
|
139
|
-
handler, or a route-level hook whose name doesn't match a known auth pattern, produces
|
|
140
|
-
`authRequired: "unknown"` instead of a confident `false`.
|
|
141
|
-
|
|
142
|
-
#### Supplying what can't be inferred
|
|
143
|
-
|
|
144
|
-
Four extra `AnalyzerOptions` fields cover metadata no static analysis can recover:
|
|
145
|
-
|
|
146
|
-
```js
|
|
147
|
-
await analyze(app, {
|
|
148
|
-
projectName: 'My API',
|
|
149
|
-
apiKey: process.env.APIFORGE_API_KEY,
|
|
150
|
-
|
|
151
|
-
// scope name -> extra metadata, joined onto each route's resolved auth scope
|
|
152
|
-
scopeCatalogPath: './scope-catalog.json',
|
|
153
|
-
|
|
154
|
-
// error code -> { status, retry, message, hint? }, joined onto a route by
|
|
155
|
-
// response status, or by the `errors` list in an annotation (see below)
|
|
156
|
-
errorCatalogPath: './error-catalog.json',
|
|
157
|
-
|
|
158
|
-
// per-prefix response envelope shape — required for every versioned prefix
|
|
159
|
-
// your API serves once you use this option at all; an unlisted prefix is
|
|
160
|
-
// reported as a warning rather than silently assumed bare
|
|
161
|
-
envelopes: {
|
|
162
|
-
'/v1': { bare: true },
|
|
163
|
-
'/v2': { wrapper: 'ApiEnvelope', dataField: 'data' },
|
|
164
|
-
},
|
|
165
|
-
// With `envelopes` set, `GET /v1/things` and `GET /v2/things` get *different*
|
|
166
|
-
// `responses['200']` schemas from this one declaration — `/v2`'s nests the same
|
|
167
|
-
// bare shape under `data` instead of replacing it, so it stays a strict superset.
|
|
168
|
-
|
|
169
|
-
// "METHOD /path" -> { summary?, description?, tags?, deprecated?, errors? }
|
|
170
|
-
// overrides for whatever the analyzer got wrong or couldn't see at all
|
|
171
|
-
annotationsPath: './annotations.json',
|
|
172
|
-
})
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
The same per-route overrides can also be attached inline at the route, via Fastify's own
|
|
176
|
-
`config` option — no sidecar file needed for a one-off:
|
|
177
|
-
|
|
178
|
-
```js
|
|
179
|
-
app.get('/users', {
|
|
180
|
-
config: {
|
|
181
|
-
apiforge: { summary: 'List users', tags: ['users'] },
|
|
182
|
-
},
|
|
183
|
-
}, listUsers)
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
An `annotationsPath` entry that doesn't match any route Fastify actually registered (a stale
|
|
187
|
-
key left behind after a route was renamed or removed) logs a warning rather than failing
|
|
188
|
-
silently.
|
|
189
|
-
|
|
190
|
-
We deliberately do **not** read Fastify's request-side `schema` option for documentation —
|
|
191
|
-
adding a schema there gives the route a second, independent validator with its own error
|
|
192
|
-
contract, and `fast-json-stringify` silently drops response fields a *response* schema doesn't
|
|
193
|
-
declare. Use zod (auto-detected) or the annotation options above instead.
|
|
194
|
-
|
|
195
|
-
### Microservices — per-service
|
|
196
|
-
|
|
197
|
-
When running each service independently, pass `service` to tag routes with the service name:
|
|
198
|
-
|
|
199
|
-
```js
|
|
200
|
-
const { analyze } = require('apiforge-analyzer')
|
|
201
|
-
|
|
202
|
-
analyze(app, {
|
|
203
|
-
projectName: 'My Platform',
|
|
204
|
-
apiKey: process.env.APIFORGE_API_KEY,
|
|
205
|
-
architectureType: 'microservices',
|
|
206
|
-
service: 'auth-service',
|
|
207
|
-
upload: true,
|
|
208
|
-
})
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### Next.js Pages Router
|
|
212
|
-
|
|
213
|
-
```ts
|
|
214
|
-
import { analyzeNextPages } from 'apiforge-analyzer'
|
|
215
|
-
|
|
216
|
-
await analyzeNextPages({
|
|
217
|
-
projectName: 'My Next.js API',
|
|
218
|
-
apiKey: process.env.APIFORGE_API_KEY,
|
|
219
|
-
nextDir: 'pages/api',
|
|
220
|
-
upload: true,
|
|
221
|
-
})
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
### Next.js App Router
|
|
225
|
-
|
|
226
|
-
```ts
|
|
227
|
-
import { analyzeNextApp } from 'apiforge-analyzer'
|
|
228
|
-
|
|
229
|
-
await analyzeNextApp({
|
|
230
|
-
projectName: 'My Next.js API',
|
|
231
|
-
apiKey: process.env.APIFORGE_API_KEY,
|
|
232
|
-
nextDir: 'app',
|
|
233
|
-
upload: true,
|
|
234
|
-
})
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
### Vite
|
|
238
|
-
|
|
239
|
-
```ts
|
|
240
|
-
import { analyzeVite } from 'apiforge-analyzer'
|
|
241
|
-
|
|
242
|
-
await analyzeVite({
|
|
243
|
-
projectName: 'My App',
|
|
244
|
-
apiKey: process.env.APIFORGE_API_KEY,
|
|
245
|
-
nextDir: 'vite.config.ts',
|
|
246
|
-
upload: true,
|
|
247
|
-
})
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### React Router
|
|
251
|
-
|
|
252
|
-
```ts
|
|
253
|
-
import { analyzeReactRouter } from 'apiforge-analyzer'
|
|
254
|
-
|
|
255
|
-
await analyzeReactRouter({
|
|
256
|
-
projectName: 'My App',
|
|
257
|
-
apiKey: process.env.APIFORGE_API_KEY,
|
|
258
|
-
nextDir: 'src/routes.tsx',
|
|
259
|
-
upload: true,
|
|
260
|
-
})
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
### Python — FastAPI, Flask, Django / DRF
|
|
264
|
-
|
|
265
|
-
Run `npx apiforge` from the Python project root (Node ≥ 18 is still needed to run the CLI).
|
|
266
|
-
The project is recognised from `manage.py` or from `fastapi` / `flask` / `django` in
|
|
267
|
-
`requirements.txt`, `pyproject.toml`, `Pipfile`, `setup.py`, or `setup.cfg`.
|
|
268
|
-
|
|
269
|
-
It works like the Node flow. The CLI:
|
|
270
|
-
|
|
271
|
-
1. Finds the file to patch: the module that creates the app (`main.py`, the Flask `create_app` module), or
|
|
272
|
-
Django's `manage.py`.
|
|
273
|
-
2. Checks that the framework is installed. If it isn't, it offers to create `.venv` and `pip install` the
|
|
274
|
-
project's dependencies from `requirements.txt`, `setup.py`/`pyproject.toml` (`pip install -e .`), or a
|
|
275
|
-
bare `pyproject.toml` dependency list. This is the `npm install` step, done for you.
|
|
276
|
-
3. Asks which command starts your server, like the npm-script picker:
|
|
277
|
-
- FastAPI: `fastapi dev`, `fastapi run`, `uvicorn main:app`, `python main.py`
|
|
278
|
-
- Flask: `flask --app app run`, `python app.py`
|
|
279
|
-
- Django: `manage.py runserver --noreload`, `manage.py runserver`
|
|
280
|
-
|
|
281
|
-
You can also type your own. All of these use the project's venv Python.
|
|
282
|
-
4. Adds a temporary snippet to that file and runs the command. The snippet reads the routes as soon as
|
|
283
|
-
the app is loaded, before the server binds its port.
|
|
284
|
-
5. If the server crashes with `No module named 'x'`, offers to install the package and retry. Only packages
|
|
285
|
-
the project declares (including extras such as `[test]`) or well-known import≠pip names (`jose` →
|
|
286
|
-
`python-jose`) are offered.
|
|
287
|
-
6. Stops the server and uploads the routes. At the end it asks "Remove APIForge code changes?", like the
|
|
288
|
-
Node flow. Ctrl+C removes the snippet immediately.
|
|
289
|
-
|
|
290
|
-
The snippet holds no API key and does nothing unless the CLI runs it: it needs an `APIFORGE_INTROSPECT`
|
|
291
|
-
environment variable that only the CLI sets. It writes routes to a temp file outside the project, and the
|
|
292
|
-
CLI uploads them. If you keep the snippet, it does nothing on its own, and the next run replaces it.
|
|
293
|
-
Because your own start command runs, the server needs what it normally needs, such as env vars like a
|
|
294
|
-
database URL. If it fails, the CLI shows the last lines of its output, where Python prints the actual error.
|
|
295
|
-
|
|
296
|
-
The routes are read from the framework's live route table:
|
|
297
|
-
|
|
298
|
-
| Framework | Source of truth |
|
|
299
|
-
|---|---|
|
|
300
|
-
| FastAPI | `app.openapi()`, which gives real Pydantic types for params, bodies, and responses. It also picks up dependency names, `HTTPException` codes raised in handlers, and `app.mount()` sub-apps. |
|
|
301
|
-
| Flask | `app.url_map` plus each view's source: `request.args`, `request.get_json()`, `abort()`, decorators. Blueprints and `MethodView` are supported. |
|
|
302
|
-
| Django | The root URLconf resolver, including `path()`/`re_path()`, class-based views, DRF ViewSets/routers, `@api_view`, serializers, and `permission_classes`. Admin and DRF login URLs are skipped. |
|
|
303
|
-
|
|
304
|
-
The suggested commands use `APIFORGE_PYTHON`, else the active virtualenv, else `.venv/`, `venv/` or `env/`
|
|
305
|
-
in the project, else the system `python`. That interpreter must have the app's dependencies installed.
|
|
306
|
-
|
|
307
|
-
Programmatic use from Node doesn't inject anything or start a server. It imports the app with that same
|
|
308
|
-
interpreter and reads the route table directly:
|
|
309
|
-
|
|
310
|
-
```ts
|
|
311
|
-
import { analyze } from 'apiforge-analyzer'
|
|
312
|
-
|
|
313
|
-
await analyze(null, {
|
|
314
|
-
projectName: 'My API',
|
|
315
|
-
framework: 'fastapi', // or 'flask' / 'django' — auto-detected when omitted
|
|
316
|
-
pythonApp: 'main:app', // Flask factory: 'app:create_app' + pythonFactory: true
|
|
317
|
-
// djangoSettings: 'mysite.settings', // Django: defaults to the one manage.py sets
|
|
318
|
-
})
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
## Options
|
|
322
|
-
|
|
323
|
-
```ts
|
|
324
|
-
interface AnalyzerOptions {
|
|
325
|
-
projectName: string // required
|
|
326
|
-
|
|
327
|
-
apiKey?: string // APIForge API key — also read from APIFORGE_API_KEY env var
|
|
328
|
-
baseUrl?: string // base URL shown in generated docs (e.g. https://api.myapp.com)
|
|
329
|
-
apiForgeUrl?: string // override backend URL (default: https://apiforgeapi.brainfogagency.com)
|
|
330
|
-
|
|
331
|
-
upload?: boolean // upload to dashboard (default: true when apiKey is set)
|
|
332
|
-
saveJson?: boolean // write routes to a local JSON file
|
|
333
|
-
jsonPath?: string // path for the JSON file (default: api-export.json)
|
|
334
|
-
|
|
335
|
-
architectureType?: 'monolithic' | 'modular-monolithic' | 'microservices'
|
|
336
|
-
service?: string // service name for microservices mode — stamped on every route
|
|
337
|
-
|
|
338
|
-
framework?: string // skip auto-detection and force a framework
|
|
339
|
-
nextDir?: string // directory or config file path for Next.js / Vite / React Router
|
|
340
|
-
|
|
341
|
-
skipInProduction?: boolean // skip when NODE_ENV=production (default: true)
|
|
342
|
-
runOnce?: boolean // debounce — skip if analyzed within the last 30 s (default: true)
|
|
343
|
-
|
|
344
|
-
// Fastify only — see "Supplying what can't be inferred" above
|
|
345
|
-
scopeCatalogPath?: string
|
|
346
|
-
errorCatalogPath?: string
|
|
347
|
-
envelopes?: Record<string, { wrapper: string; dataField: string; excluded?: string[] } | { bare: true }>
|
|
348
|
-
annotationsPath?: string
|
|
349
|
-
|
|
350
|
-
// Python only — see "Python" above
|
|
351
|
-
pythonApp?: string // app import path, e.g. 'main:app' (auto-detected when omitted)
|
|
352
|
-
pythonFactory?: boolean // call pythonApp as a factory (Flask create_app)
|
|
353
|
-
pythonPath?: string // interpreter (default: APIFORGE_PYTHON / active venv / project venv)
|
|
354
|
-
djangoSettings?: string // e.g. 'mysite.settings' (default: read from manage.py)
|
|
355
|
-
}
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
`analyze()` throws instead of resolving to `[]` when the detected or forced framework has no
|
|
359
|
-
analyzer (currently: anything other than `express`, `fastify`, `nestjs`, `nextjs-pages`,
|
|
360
|
-
`nextjs-app`, `vite`, `react-router`, `fastapi`, `flask`, `django`). Every other early-exit (no API key, debounced, zero
|
|
361
|
-
routes found, `NODE_ENV=production`) still resolves to `[]` — only "this framework cannot be
|
|
362
|
-
analyzed" is a thrown error, so a caller can tell the two apart. `detectFramework()` also
|
|
363
|
-
exposes this directly: `detectFramework(app).supported` is `false` for a framework it can
|
|
364
|
-
name but not analyze (currently `koa`, `hapi`, `unknown`).
|
|
365
|
-
|
|
366
|
-
## Architecture detection
|
|
367
|
-
|
|
368
|
-
The CLI scores structural signals to pick the right architecture automatically:
|
|
369
|
-
|
|
370
|
-
| Signal | Architecture |
|
|
371
|
-
|---|---|
|
|
372
|
-
| `apiforge.config.json` with `services[]` | microservices |
|
|
373
|
-
| `docker-compose.yml` with 3+ node services | microservices |
|
|
374
|
-
| Multiple `package.json` files in sibling dirs | microservices |
|
|
375
|
-
| `src/modules/` or `src/features/` directory | modular-monolithic |
|
|
376
|
-
| `*.module.ts` files (NestJS `@Module`) | modular-monolithic |
|
|
377
|
-
| None of the above | monolithic |
|
|
378
|
-
|
|
379
|
-
For modular-monolithic projects, routes are automatically grouped by module using path-prefix matching — `/api/users/*` gets tagged as the `users` module.
|
|
380
|
-
|
|
381
|
-
## Output format
|
|
382
|
-
|
|
383
|
-
Each collected route:
|
|
384
|
-
|
|
385
|
-
```ts
|
|
386
|
-
{
|
|
387
|
-
method: 'GET',
|
|
388
|
-
path: '/api/users/:id',
|
|
389
|
-
tag: 'users',
|
|
390
|
-
operationId: 'get-api-users-id',
|
|
391
|
-
summary: 'Get api users',
|
|
392
|
-
middleware: ['authenticate'],
|
|
393
|
-
authRequired: true,
|
|
394
|
-
pathParams: ['id'],
|
|
395
|
-
queryParams: [],
|
|
396
|
-
requestBodyFields: [],
|
|
397
|
-
responseStatuses: [200, 404],
|
|
398
|
-
framework: 'express',
|
|
399
|
-
module?: 'users', // modular-monolithic only
|
|
400
|
-
service?: 'auth-service' // microservices only
|
|
401
|
-
}
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
## Escape hatch — building routes yourself
|
|
405
|
-
|
|
406
|
-
If your framework has no `analyze*()` function, call `uploadRoutes()` directly with a
|
|
407
|
-
hand-built `Route[]`. This is the same function every `analyze*()` call ends up using
|
|
408
|
-
internally — it's a supported, typed, top-level export, not a private helper.
|
|
409
|
-
|
|
410
|
-
```ts
|
|
411
|
-
import { uploadRoutes, type Route } from 'apiforge-analyzer'
|
|
412
|
-
|
|
413
|
-
const routes: Route[] = [
|
|
414
|
-
{
|
|
415
|
-
method: 'GET',
|
|
416
|
-
path: '/api/users/:id',
|
|
417
|
-
tag: 'users',
|
|
418
|
-
operationId: 'get_api_users_id',
|
|
419
|
-
summary: 'Get a user by id',
|
|
420
|
-
middleware: [],
|
|
421
|
-
authRequired: true,
|
|
422
|
-
pathParams: ['id'],
|
|
423
|
-
queryParams: [],
|
|
424
|
-
requestBodyFields: [],
|
|
425
|
-
linkedPaths: [],
|
|
426
|
-
hasAuth: true,
|
|
427
|
-
framework: 'my-framework',
|
|
428
|
-
// routeKey?: stable dedup key the dashboard uses across uploads — see below
|
|
429
|
-
},
|
|
430
|
-
]
|
|
431
|
-
|
|
432
|
-
await uploadRoutes(routes, {
|
|
433
|
-
projectName: 'My API',
|
|
434
|
-
apiKey: process.env.APIFORGE_API_KEY!,
|
|
435
|
-
})
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
The fields above (`method`, `path`, `tag`, `operationId`, `summary`, `middleware`,
|
|
439
|
-
`authRequired`, `pathParams`, `queryParams`, `requestBodyFields`, `linkedPaths`, `hasAuth`,
|
|
440
|
-
`framework`) are the only ones `Route` requires; everything else (`description`, `tags`,
|
|
441
|
-
`headerParams`, `responseStatuses`, `parameters`, `requestBody`, `responses`, `module`,
|
|
442
|
-
`service`, ...) is optional and gets a reasonable default from `enrichRoutes()`.
|
|
443
|
-
|
|
444
|
-
**Set `routeKey` yourself.** If you don't, it defaults to `api-001`, `api-002`, ... based on
|
|
445
|
-
array position — which shifts if a route is added, removed, or reordered between uploads, so
|
|
446
|
-
the dashboard can no longer tell "this is the same endpoint as last time" from "this is a new
|
|
447
|
-
one". Use something stable per endpoint, e.g. `` `${method} ${path}` ``, as every built-in
|
|
448
|
-
analyzer does.
|
|
449
|
-
|
|
450
|
-
## ESM and CommonJS
|
|
451
|
-
|
|
452
|
-
```js
|
|
453
|
-
// ESM
|
|
454
|
-
import { analyze } from 'apiforge-analyzer'
|
|
455
|
-
|
|
456
|
-
// CommonJS
|
|
457
|
-
const { analyze } = require('apiforge-analyzer')
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
## Environment variables
|
|
461
|
-
|
|
462
|
-
| Variable | Description |
|
|
463
|
-
|---|---|
|
|
464
|
-
| `APIFORGE_API_KEY` | API key — loaded automatically, no dotenv required |
|
|
465
|
-
| `APIFORGE_URL` | Override backend URL |
|
|
466
|
-
| `NODE_ENV` | Set to `production` to disable analysis |
|
|
467
|
-
|
|
468
|
-
## Get an API key
|
|
469
|
-
|
|
470
|
-
[apiforge.brainfogagency.com/dashboard](https://apiforge.brainfogagency.com/dashboard)
|
|
471
|
-
|
|
472
|
-
## License
|
|
473
|
-
|
|
474
|
-
MIT
|
|
1
|
+
# apiforge-analyzer
|
|
2
|
+
|
|
3
|
+
CLI and library for auto-generating API documentation from live Express, Fastify, NestJS, Next.js, Vite, and React Router apps — and Python FastAPI, Flask, and Django apps.
|
|
4
|
+
|
|
5
|
+
Routes are extracted at runtime by walking the framework's internal route stack — no static analysis, no decorators, no changes to your production code.
|
|
6
|
+
|
|
7
|
+
## CLI usage
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx apiforge
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Run this from your project root. The CLI will:
|
|
14
|
+
|
|
15
|
+
1. Detect your architecture (monolithic / modular-monolithic / microservices)
|
|
16
|
+
2. Find your server entry file (or ask)
|
|
17
|
+
3. Inject a temporary analysis snippet
|
|
18
|
+
4. Start your server with the command you choose
|
|
19
|
+
5. Collect all registered routes
|
|
20
|
+
6. Upload to your APIForge dashboard for review
|
|
21
|
+
7. Remove the injected code
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
// apiforge.config.json — auto-created on first run, commit this file
|
|
27
|
+
{
|
|
28
|
+
"projectName": "My Backend",
|
|
29
|
+
"architectureType": "microservices",
|
|
30
|
+
"services": [
|
|
31
|
+
{ "name": "auth-service", "dir": "./services/auth-service" },
|
|
32
|
+
{ "name": "user-service", "dir": "./services/user-service" },
|
|
33
|
+
{ "name": "order-service", "dir": "./services/order-service" }
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Each service entry can also include `"startCommand"` to override the default (`node <entryFile>`):
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{ "name": "auth-service", "dir": "./services/auth", "startCommand": "npx tsx services/auth/src/index.ts" }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Programmatic API
|
|
45
|
+
|
|
46
|
+
Install:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install apiforge-analyzer
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Express
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
const express = require('express')
|
|
56
|
+
const { analyze } = require('apiforge-analyzer')
|
|
57
|
+
|
|
58
|
+
const app = express()
|
|
59
|
+
app.use(express.json())
|
|
60
|
+
|
|
61
|
+
app.get('/api/users', getUsers)
|
|
62
|
+
app.post('/api/users', createUser)
|
|
63
|
+
app.get('/api/users/:id', getUserById)
|
|
64
|
+
|
|
65
|
+
// Call after all routes are registered
|
|
66
|
+
analyze(app, {
|
|
67
|
+
projectName: 'My API',
|
|
68
|
+
apiKey: process.env.APIFORGE_API_KEY,
|
|
69
|
+
baseUrl: 'https://api.myapp.com',
|
|
70
|
+
upload: true,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
app.listen(3000)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### NestJS
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { analyze } from 'apiforge-analyzer'
|
|
80
|
+
|
|
81
|
+
const app = await NestFactory.create(AppModule)
|
|
82
|
+
await app.init()
|
|
83
|
+
|
|
84
|
+
await analyze(app.getHttpServer(), {
|
|
85
|
+
projectName: 'My NestJS API',
|
|
86
|
+
apiKey: process.env.APIFORGE_API_KEY,
|
|
87
|
+
upload: true,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
await app.listen(3000)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Fastify
|
|
94
|
+
|
|
95
|
+
Routes are captured via Fastify's `onRoute` hook, so composed paths (`register(fn, { prefix })`,
|
|
96
|
+
relative registrations, nested scopes) resolve to their real runtime URL rather than the literal
|
|
97
|
+
string passed to `app.get(...)`. Handlers are resolved with a TypeScript checker (not a regex),
|
|
98
|
+
so a handler that lives in another module — including one reached through an aliased import,
|
|
99
|
+
a curried factory, or a thin delegating wrapper — is still followed to its real implementation.
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
const fastify = require('fastify')
|
|
103
|
+
const { analyze } = require('apiforge-analyzer')
|
|
104
|
+
|
|
105
|
+
const app = fastify()
|
|
106
|
+
|
|
107
|
+
app.register(async (v1) => {
|
|
108
|
+
v1.get('/users', listUsers)
|
|
109
|
+
v1.post('/users', createUser)
|
|
110
|
+
}, { prefix: '/v1' })
|
|
111
|
+
|
|
112
|
+
// Call after all routes are registered, before or after app.listen — analyze()
|
|
113
|
+
// awaits app.ready() itself.
|
|
114
|
+
await analyze(app, {
|
|
115
|
+
projectName: 'My API',
|
|
116
|
+
apiKey: process.env.APIFORGE_API_KEY,
|
|
117
|
+
upload: true,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
await app.listen({ port: 3000 })
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
What's inferred automatically, with no annotation: the composed path, the handler's resolved
|
|
124
|
+
source location and leading JSDoc comment (lifted verbatim into `description`), header reads,
|
|
125
|
+
literal and simple-ternary response statuses, and auth — a route counts as authenticated if a
|
|
126
|
+
route-level `onRequest`/`preHandler` hook looks like an auth check by name or body, or if the
|
|
127
|
+
resolved handler calls a guard function (`requireScope`, `requireOperator`, or a local alias of
|
|
128
|
+
either — `const operator = requireOperator` is followed, not missed).
|
|
129
|
+
|
|
130
|
+
Query and body fields backed by a zod schema — matching both `schema.parse(request.query)` and
|
|
131
|
+
a wrapper call like `parse(schema, request.query)` — come out typed, not as bare field names:
|
|
132
|
+
`z.string().min/max/length()`, `.number()`, `.boolean()`, `.enum([...])` and `.default()` are
|
|
133
|
+
read off the schema and land in the standard `Route.parameters` / `Route.requestBody` fields as
|
|
134
|
+
real JSON Schema (`minLength`/`maxLength`, `enum`, `default`, etc.), the same fields
|
|
135
|
+
`uploadRoutes()` and `exportToJSON()` read — not a side channel that gets dropped on the way out.
|
|
136
|
+
A `.refine()` on the object doesn't drop the fields declared before it.
|
|
137
|
+
|
|
138
|
+
What can't be inferred safely is reported as `"unknown"` rather than guessed — an unresolved
|
|
139
|
+
handler, or a route-level hook whose name doesn't match a known auth pattern, produces
|
|
140
|
+
`authRequired: "unknown"` instead of a confident `false`.
|
|
141
|
+
|
|
142
|
+
#### Supplying what can't be inferred
|
|
143
|
+
|
|
144
|
+
Four extra `AnalyzerOptions` fields cover metadata no static analysis can recover:
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
await analyze(app, {
|
|
148
|
+
projectName: 'My API',
|
|
149
|
+
apiKey: process.env.APIFORGE_API_KEY,
|
|
150
|
+
|
|
151
|
+
// scope name -> extra metadata, joined onto each route's resolved auth scope
|
|
152
|
+
scopeCatalogPath: './scope-catalog.json',
|
|
153
|
+
|
|
154
|
+
// error code -> { status, retry, message, hint? }, joined onto a route by
|
|
155
|
+
// response status, or by the `errors` list in an annotation (see below)
|
|
156
|
+
errorCatalogPath: './error-catalog.json',
|
|
157
|
+
|
|
158
|
+
// per-prefix response envelope shape — required for every versioned prefix
|
|
159
|
+
// your API serves once you use this option at all; an unlisted prefix is
|
|
160
|
+
// reported as a warning rather than silently assumed bare
|
|
161
|
+
envelopes: {
|
|
162
|
+
'/v1': { bare: true },
|
|
163
|
+
'/v2': { wrapper: 'ApiEnvelope', dataField: 'data' },
|
|
164
|
+
},
|
|
165
|
+
// With `envelopes` set, `GET /v1/things` and `GET /v2/things` get *different*
|
|
166
|
+
// `responses['200']` schemas from this one declaration — `/v2`'s nests the same
|
|
167
|
+
// bare shape under `data` instead of replacing it, so it stays a strict superset.
|
|
168
|
+
|
|
169
|
+
// "METHOD /path" -> { summary?, description?, tags?, deprecated?, errors? }
|
|
170
|
+
// overrides for whatever the analyzer got wrong or couldn't see at all
|
|
171
|
+
annotationsPath: './annotations.json',
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The same per-route overrides can also be attached inline at the route, via Fastify's own
|
|
176
|
+
`config` option — no sidecar file needed for a one-off:
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
app.get('/users', {
|
|
180
|
+
config: {
|
|
181
|
+
apiforge: { summary: 'List users', tags: ['users'] },
|
|
182
|
+
},
|
|
183
|
+
}, listUsers)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
An `annotationsPath` entry that doesn't match any route Fastify actually registered (a stale
|
|
187
|
+
key left behind after a route was renamed or removed) logs a warning rather than failing
|
|
188
|
+
silently.
|
|
189
|
+
|
|
190
|
+
We deliberately do **not** read Fastify's request-side `schema` option for documentation —
|
|
191
|
+
adding a schema there gives the route a second, independent validator with its own error
|
|
192
|
+
contract, and `fast-json-stringify` silently drops response fields a *response* schema doesn't
|
|
193
|
+
declare. Use zod (auto-detected) or the annotation options above instead.
|
|
194
|
+
|
|
195
|
+
### Microservices — per-service
|
|
196
|
+
|
|
197
|
+
When running each service independently, pass `service` to tag routes with the service name:
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
const { analyze } = require('apiforge-analyzer')
|
|
201
|
+
|
|
202
|
+
analyze(app, {
|
|
203
|
+
projectName: 'My Platform',
|
|
204
|
+
apiKey: process.env.APIFORGE_API_KEY,
|
|
205
|
+
architectureType: 'microservices',
|
|
206
|
+
service: 'auth-service',
|
|
207
|
+
upload: true,
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Next.js Pages Router
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import { analyzeNextPages } from 'apiforge-analyzer'
|
|
215
|
+
|
|
216
|
+
await analyzeNextPages({
|
|
217
|
+
projectName: 'My Next.js API',
|
|
218
|
+
apiKey: process.env.APIFORGE_API_KEY,
|
|
219
|
+
nextDir: 'pages/api',
|
|
220
|
+
upload: true,
|
|
221
|
+
})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Next.js App Router
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { analyzeNextApp } from 'apiforge-analyzer'
|
|
228
|
+
|
|
229
|
+
await analyzeNextApp({
|
|
230
|
+
projectName: 'My Next.js API',
|
|
231
|
+
apiKey: process.env.APIFORGE_API_KEY,
|
|
232
|
+
nextDir: 'app',
|
|
233
|
+
upload: true,
|
|
234
|
+
})
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Vite
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { analyzeVite } from 'apiforge-analyzer'
|
|
241
|
+
|
|
242
|
+
await analyzeVite({
|
|
243
|
+
projectName: 'My App',
|
|
244
|
+
apiKey: process.env.APIFORGE_API_KEY,
|
|
245
|
+
nextDir: 'vite.config.ts',
|
|
246
|
+
upload: true,
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### React Router
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import { analyzeReactRouter } from 'apiforge-analyzer'
|
|
254
|
+
|
|
255
|
+
await analyzeReactRouter({
|
|
256
|
+
projectName: 'My App',
|
|
257
|
+
apiKey: process.env.APIFORGE_API_KEY,
|
|
258
|
+
nextDir: 'src/routes.tsx',
|
|
259
|
+
upload: true,
|
|
260
|
+
})
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Python — FastAPI, Flask, Django / DRF
|
|
264
|
+
|
|
265
|
+
Run `npx apiforge` from the Python project root (Node ≥ 18 is still needed to run the CLI).
|
|
266
|
+
The project is recognised from `manage.py` or from `fastapi` / `flask` / `django` in
|
|
267
|
+
`requirements.txt`, `pyproject.toml`, `Pipfile`, `setup.py`, or `setup.cfg`.
|
|
268
|
+
|
|
269
|
+
It works like the Node flow. The CLI:
|
|
270
|
+
|
|
271
|
+
1. Finds the file to patch: the module that creates the app (`main.py`, the Flask `create_app` module), or
|
|
272
|
+
Django's `manage.py`.
|
|
273
|
+
2. Checks that the framework is installed. If it isn't, it offers to create `.venv` and `pip install` the
|
|
274
|
+
project's dependencies from `requirements.txt`, `setup.py`/`pyproject.toml` (`pip install -e .`), or a
|
|
275
|
+
bare `pyproject.toml` dependency list. This is the `npm install` step, done for you.
|
|
276
|
+
3. Asks which command starts your server, like the npm-script picker:
|
|
277
|
+
- FastAPI: `fastapi dev`, `fastapi run`, `uvicorn main:app`, `python main.py`
|
|
278
|
+
- Flask: `flask --app app run`, `python app.py`
|
|
279
|
+
- Django: `manage.py runserver --noreload`, `manage.py runserver`
|
|
280
|
+
|
|
281
|
+
You can also type your own. All of these use the project's venv Python.
|
|
282
|
+
4. Adds a temporary snippet to that file and runs the command. The snippet reads the routes as soon as
|
|
283
|
+
the app is loaded, before the server binds its port.
|
|
284
|
+
5. If the server crashes with `No module named 'x'`, offers to install the package and retry. Only packages
|
|
285
|
+
the project declares (including extras such as `[test]`) or well-known import≠pip names (`jose` →
|
|
286
|
+
`python-jose`) are offered.
|
|
287
|
+
6. Stops the server and uploads the routes. At the end it asks "Remove APIForge code changes?", like the
|
|
288
|
+
Node flow. Ctrl+C removes the snippet immediately.
|
|
289
|
+
|
|
290
|
+
The snippet holds no API key and does nothing unless the CLI runs it: it needs an `APIFORGE_INTROSPECT`
|
|
291
|
+
environment variable that only the CLI sets. It writes routes to a temp file outside the project, and the
|
|
292
|
+
CLI uploads them. If you keep the snippet, it does nothing on its own, and the next run replaces it.
|
|
293
|
+
Because your own start command runs, the server needs what it normally needs, such as env vars like a
|
|
294
|
+
database URL. If it fails, the CLI shows the last lines of its output, where Python prints the actual error.
|
|
295
|
+
|
|
296
|
+
The routes are read from the framework's live route table:
|
|
297
|
+
|
|
298
|
+
| Framework | Source of truth |
|
|
299
|
+
|---|---|
|
|
300
|
+
| FastAPI | `app.openapi()`, which gives real Pydantic types for params, bodies, and responses. It also picks up dependency names, `HTTPException` codes raised in handlers, and `app.mount()` sub-apps. |
|
|
301
|
+
| Flask | `app.url_map` plus each view's source: `request.args`, `request.get_json()`, `abort()`, decorators. Blueprints and `MethodView` are supported. |
|
|
302
|
+
| Django | The root URLconf resolver, including `path()`/`re_path()`, class-based views, DRF ViewSets/routers, `@api_view`, serializers, and `permission_classes`. Admin and DRF login URLs are skipped. |
|
|
303
|
+
|
|
304
|
+
The suggested commands use `APIFORGE_PYTHON`, else the active virtualenv, else `.venv/`, `venv/` or `env/`
|
|
305
|
+
in the project, else the system `python`. That interpreter must have the app's dependencies installed.
|
|
306
|
+
|
|
307
|
+
Programmatic use from Node doesn't inject anything or start a server. It imports the app with that same
|
|
308
|
+
interpreter and reads the route table directly:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
import { analyze } from 'apiforge-analyzer'
|
|
312
|
+
|
|
313
|
+
await analyze(null, {
|
|
314
|
+
projectName: 'My API',
|
|
315
|
+
framework: 'fastapi', // or 'flask' / 'django' — auto-detected when omitted
|
|
316
|
+
pythonApp: 'main:app', // Flask factory: 'app:create_app' + pythonFactory: true
|
|
317
|
+
// djangoSettings: 'mysite.settings', // Django: defaults to the one manage.py sets
|
|
318
|
+
})
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Options
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
interface AnalyzerOptions {
|
|
325
|
+
projectName: string // required
|
|
326
|
+
|
|
327
|
+
apiKey?: string // APIForge API key — also read from APIFORGE_API_KEY env var
|
|
328
|
+
baseUrl?: string // base URL shown in generated docs (e.g. https://api.myapp.com)
|
|
329
|
+
apiForgeUrl?: string // override backend URL (default: https://apiforgeapi.brainfogagency.com)
|
|
330
|
+
|
|
331
|
+
upload?: boolean // upload to dashboard (default: true when apiKey is set)
|
|
332
|
+
saveJson?: boolean // write routes to a local JSON file
|
|
333
|
+
jsonPath?: string // path for the JSON file (default: api-export.json)
|
|
334
|
+
|
|
335
|
+
architectureType?: 'monolithic' | 'modular-monolithic' | 'microservices'
|
|
336
|
+
service?: string // service name for microservices mode — stamped on every route
|
|
337
|
+
|
|
338
|
+
framework?: string // skip auto-detection and force a framework
|
|
339
|
+
nextDir?: string // directory or config file path for Next.js / Vite / React Router
|
|
340
|
+
|
|
341
|
+
skipInProduction?: boolean // skip when NODE_ENV=production (default: true)
|
|
342
|
+
runOnce?: boolean // debounce — skip if analyzed within the last 30 s (default: true)
|
|
343
|
+
|
|
344
|
+
// Fastify only — see "Supplying what can't be inferred" above
|
|
345
|
+
scopeCatalogPath?: string
|
|
346
|
+
errorCatalogPath?: string
|
|
347
|
+
envelopes?: Record<string, { wrapper: string; dataField: string; excluded?: string[] } | { bare: true }>
|
|
348
|
+
annotationsPath?: string
|
|
349
|
+
|
|
350
|
+
// Python only — see "Python" above
|
|
351
|
+
pythonApp?: string // app import path, e.g. 'main:app' (auto-detected when omitted)
|
|
352
|
+
pythonFactory?: boolean // call pythonApp as a factory (Flask create_app)
|
|
353
|
+
pythonPath?: string // interpreter (default: APIFORGE_PYTHON / active venv / project venv)
|
|
354
|
+
djangoSettings?: string // e.g. 'mysite.settings' (default: read from manage.py)
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
`analyze()` throws instead of resolving to `[]` when the detected or forced framework has no
|
|
359
|
+
analyzer (currently: anything other than `express`, `fastify`, `nestjs`, `nextjs-pages`,
|
|
360
|
+
`nextjs-app`, `vite`, `react-router`, `fastapi`, `flask`, `django`). Every other early-exit (no API key, debounced, zero
|
|
361
|
+
routes found, `NODE_ENV=production`) still resolves to `[]` — only "this framework cannot be
|
|
362
|
+
analyzed" is a thrown error, so a caller can tell the two apart. `detectFramework()` also
|
|
363
|
+
exposes this directly: `detectFramework(app).supported` is `false` for a framework it can
|
|
364
|
+
name but not analyze (currently `koa`, `hapi`, `unknown`).
|
|
365
|
+
|
|
366
|
+
## Architecture detection
|
|
367
|
+
|
|
368
|
+
The CLI scores structural signals to pick the right architecture automatically:
|
|
369
|
+
|
|
370
|
+
| Signal | Architecture |
|
|
371
|
+
|---|---|
|
|
372
|
+
| `apiforge.config.json` with `services[]` | microservices |
|
|
373
|
+
| `docker-compose.yml` with 3+ node services | microservices |
|
|
374
|
+
| Multiple `package.json` files in sibling dirs | microservices |
|
|
375
|
+
| `src/modules/` or `src/features/` directory | modular-monolithic |
|
|
376
|
+
| `*.module.ts` files (NestJS `@Module`) | modular-monolithic |
|
|
377
|
+
| None of the above | monolithic |
|
|
378
|
+
|
|
379
|
+
For modular-monolithic projects, routes are automatically grouped by module using path-prefix matching — `/api/users/*` gets tagged as the `users` module.
|
|
380
|
+
|
|
381
|
+
## Output format
|
|
382
|
+
|
|
383
|
+
Each collected route:
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
{
|
|
387
|
+
method: 'GET',
|
|
388
|
+
path: '/api/users/:id',
|
|
389
|
+
tag: 'users',
|
|
390
|
+
operationId: 'get-api-users-id',
|
|
391
|
+
summary: 'Get api users',
|
|
392
|
+
middleware: ['authenticate'],
|
|
393
|
+
authRequired: true,
|
|
394
|
+
pathParams: ['id'],
|
|
395
|
+
queryParams: [],
|
|
396
|
+
requestBodyFields: [],
|
|
397
|
+
responseStatuses: [200, 404],
|
|
398
|
+
framework: 'express',
|
|
399
|
+
module?: 'users', // modular-monolithic only
|
|
400
|
+
service?: 'auth-service' // microservices only
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Escape hatch — building routes yourself
|
|
405
|
+
|
|
406
|
+
If your framework has no `analyze*()` function, call `uploadRoutes()` directly with a
|
|
407
|
+
hand-built `Route[]`. This is the same function every `analyze*()` call ends up using
|
|
408
|
+
internally — it's a supported, typed, top-level export, not a private helper.
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
import { uploadRoutes, type Route } from 'apiforge-analyzer'
|
|
412
|
+
|
|
413
|
+
const routes: Route[] = [
|
|
414
|
+
{
|
|
415
|
+
method: 'GET',
|
|
416
|
+
path: '/api/users/:id',
|
|
417
|
+
tag: 'users',
|
|
418
|
+
operationId: 'get_api_users_id',
|
|
419
|
+
summary: 'Get a user by id',
|
|
420
|
+
middleware: [],
|
|
421
|
+
authRequired: true,
|
|
422
|
+
pathParams: ['id'],
|
|
423
|
+
queryParams: [],
|
|
424
|
+
requestBodyFields: [],
|
|
425
|
+
linkedPaths: [],
|
|
426
|
+
hasAuth: true,
|
|
427
|
+
framework: 'my-framework',
|
|
428
|
+
// routeKey?: stable dedup key the dashboard uses across uploads — see below
|
|
429
|
+
},
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
await uploadRoutes(routes, {
|
|
433
|
+
projectName: 'My API',
|
|
434
|
+
apiKey: process.env.APIFORGE_API_KEY!,
|
|
435
|
+
})
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
The fields above (`method`, `path`, `tag`, `operationId`, `summary`, `middleware`,
|
|
439
|
+
`authRequired`, `pathParams`, `queryParams`, `requestBodyFields`, `linkedPaths`, `hasAuth`,
|
|
440
|
+
`framework`) are the only ones `Route` requires; everything else (`description`, `tags`,
|
|
441
|
+
`headerParams`, `responseStatuses`, `parameters`, `requestBody`, `responses`, `module`,
|
|
442
|
+
`service`, ...) is optional and gets a reasonable default from `enrichRoutes()`.
|
|
443
|
+
|
|
444
|
+
**Set `routeKey` yourself.** If you don't, it defaults to `api-001`, `api-002`, ... based on
|
|
445
|
+
array position — which shifts if a route is added, removed, or reordered between uploads, so
|
|
446
|
+
the dashboard can no longer tell "this is the same endpoint as last time" from "this is a new
|
|
447
|
+
one". Use something stable per endpoint, e.g. `` `${method} ${path}` ``, as every built-in
|
|
448
|
+
analyzer does.
|
|
449
|
+
|
|
450
|
+
## ESM and CommonJS
|
|
451
|
+
|
|
452
|
+
```js
|
|
453
|
+
// ESM
|
|
454
|
+
import { analyze } from 'apiforge-analyzer'
|
|
455
|
+
|
|
456
|
+
// CommonJS
|
|
457
|
+
const { analyze } = require('apiforge-analyzer')
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Environment variables
|
|
461
|
+
|
|
462
|
+
| Variable | Description |
|
|
463
|
+
|---|---|
|
|
464
|
+
| `APIFORGE_API_KEY` | API key — loaded automatically, no dotenv required |
|
|
465
|
+
| `APIFORGE_URL` | Override backend URL |
|
|
466
|
+
| `NODE_ENV` | Set to `production` to disable analysis |
|
|
467
|
+
|
|
468
|
+
## Get an API key
|
|
469
|
+
|
|
470
|
+
[apiforge.brainfogagency.com/dashboard](https://apiforge.brainfogagency.com/dashboard)
|
|
471
|
+
|
|
472
|
+
## License
|
|
473
|
+
|
|
474
|
+
MIT
|