Auto API Tester
Made by rAvi
Test any Express or NestJS route straight from your source code. Put the cursor on a route, run Auto API Tester: Test This Route (or click ▶ Test above the route) and you get:
- the route's full URL, with
app.use('/prefix', router) mounts resolved across files
- a request body, query, path params and headers generated from the route's Zod/Joi schema, TypeScript types or inline validation
- a real request to your local server (after confirmation for anything but GET/HEAD/OPTIONS)
- the response checked for status, valid JSON, the shape the handler's TypeScript types promise, and timing
No configuration is needed to start. The base URL comes from your .env, package.json scripts or app.listen(port), and you're asked once if none of those turn it up.
Usage
| Action |
How |
| Test the route under the cursor |
Cmd+Alt+T / Ctrl+Alt+T, the editor context menu, or ▶ Test CodeLens |
| Fix the URL or path params |
Edit them in the panel; saved for that route until Reset saved edits |
| Edit the generated body and re-run |
Click the body in the result panel, edit, Run with edited body |
| New random data |
New data in the panel (seeded mode always regenerates the same data) |
| Copy the request |
Copy as curl (credentials are masked) |
| Save a test / reopen results |
Save test, then Auto API Tester: Show Test History |
| Turn on AI analysis |
Auto API Tester: Set Gemini API Key |
| Base URL, auth, dry-run, data mode |
Auto API Tester: Configure Project |
| Turn the confirmation prompt on/off |
Auto API Tester: Toggle Dry-Run Guard |
If the cursor is not on a route, you get a list of the routes in the file.
What is detected
NestJS: @Controller('users') classes with @Get(), @Post()… methods, the global prefix from app.setGlobalPrefix('api'), URI versioning (@Controller({ path, version: '1' })), and inputs from @Body(), @Query(), @Param() and @Headers(). Types come from class-validator DTOs (including PartialType/PickType/OmitType and nested DTOs) and parse pipes such as ParseIntPipe. The expected status comes from @HttpCode(), or Nest's default (201 for POST).
Routes: app.get/post/put/patch/delete(path, …handlers), router.x(...), element access like app['post'], app.route('/x').get(h).post(h), route tables registered in loops (routes.forEach(…) calling app[r.method] with r.path), plus Express 5 path syntax.
Mount prefixes: app.use('/api', router) in the same file, across ES imports, CommonJS require, barrel re-exports (routes/index.ts), nested routers, app.use(['/a', '/b'], router), class routers (new UserRoutes().router) and factories (app.use('/x', createRouter())). If a router is mounted at several paths, you choose one.
Request fields, in strict priority order. The first source that finds fields wins, and nothing is merged silently:
- Zod: schemas passed to middleware (
validate(schema), validate({ body, query, params })) or parsed in the handler (schema.parse(req.body)), including .extend/.merge/.partial/.pick/.omit, imported across files
- Joi:
celebrate({ [Segments.BODY]: Joi.object(...) }), schema.validate(req.query), express-validation plain objects
- express-validator:
body('email').isEmail(), check(), query(), param(), rule arrays, checkSchema({...})
- TypeScript:
Request<Params, ResBody, ReqBody, Query>, req.body as Dto, const dto: Dto = req.body
- Database model: Mongoose, Prisma, Sequelize and TypeORM models, when the data is saved as-is (
new Order(req.body), prisma.post.create({ data: req.body }), Customer.create(req.body), repo.save(req.body))
- Gemini AI (optional, see below)
- Inline validation:
if (!email.includes('@')), typeof age !== 'number', password.length < 8, validator.isEmail(x), ['a','b'].includes(role)
- Field names:
email, userId, phone, createdAt, isActive, page…
- Fallback: an empty body
The request data is followed into the functions it's passed to (createUser(req.body), service.create({ ...req.body.user })), so fields read inside services count too, and database models type fields by name. Comments marked guessed come from names or AI rather than a rule in code.
When sources disagree (for example, the handler reads req.body.nickname but the Zod schema has no such field), the panel's Analysis notes say so.
Negative tests: after a run, Send N invalid requests sends variants with a missing required field, a wrong type, a bad format or an out-of-range value. Each should get a 4xx. Any the API accepts (2xx) or crashes on (5xx) is listed as a problem. Only rules written in code are tested, never guesses.
Rejected fields: when the server answers 400/422, the result lists the fields it rejected. It understands express-validator, Joi/celebrate, Zod, class-validator and Rails-style error bodies.
Response: res.status(201).json(user) sets the expected status, and the TypeScript type of user becomes the shape the response is validated against.
Path params such as :orderId
Values come from, in order: what you typed in the panel for this route → an ID returned earlier by the API (for example the orderId in the POST /api/orders response, or an id from GET /api/orders) → generated data. The panel shows where each value came from. Run the create or list route first, and the detail route gets a real ID.
AI analysis with Gemini (optional)
When a route has no Zod, Joi or TypeScript schema, Gemini can read the route, its controller, validation middleware and types, and tell you which fields are needed, which are optional, and why. It also suggests the base URL when it can't be detected.
- Get a free key at aistudio.google.com/apikey.
- Run Auto API Tester: Set Gemini API Key. The key goes to VS Code SecretStorage and AI analysis turns on. You can also set
GEMINI_API_KEY in your environment or your project's .env.
It is built for the free tier:
- Gemini is called only when static analysis is weak (
autoApiTester.ai.mode: "fallback"). Switch to "always" to also get field descriptions on schema-based routes.
- Answers are cached per route and reused until the route's code changes.
- Each request sends only the route, its handler, validators and types (at most ~12 KB), and asks for JSON output without "thinking".
- When the free limit is reached, AI is paused for the time Google asks for and static analysis is used meanwhile.
Privacy: AI is off until you add a key. When it's on, the code above is sent to Google. .env values are never sent, except PORT/HOST/URL-style keys that don't look like credentials.
Safety
- Dry-run guard (on by default). POST, PUT, PATCH and DELETE show "This will send a real POST to http://localhost:3000/api/users" with Run once / Run and remember before anything is sent.
- Secrets (tokens, passwords, API keys) live in VS Code SecretStorage, scoped to the workspace. They are never written to settings or project files, and they are masked in the panel, in history and in curl output.
- History keeps the last 100 results, and bodies over 100 KB are not stored.
Configuration
Optional .vscode/auto-api-tester.json (validated with IntelliSense). It overrides VS Code settings:
{
"baseUrl": "http://localhost:3000",
"timeout": 5000,
"headers": { "X-Tenant": "dev" },
"auth": { "type": "bearer" },
"dryRun": { "default": true, "allowedMethods": ["GET", "HEAD", "OPTIONS"] },
"generator": { "mode": "seeded", "locale": "en-IN" }
}
auth.type is one of none, bearer, basic (with username), apiKey (with name and in: "header" | "query") or cookie. Set the secret with Configure Project → Authentication, or you'll be asked for it on the first request.
Base URL order: project file → autoApiTester.baseUrl (if you set it) → .env / .env.local (API_URL, BASE_URL or PORT + HOST) → package.json scripts → app.listen(port) in src/server|index|app → ask once.
| Setting |
Default |
|
autoApiTester.baseUrl |
http://localhost:3000 |
Used when set explicitly |
autoApiTester.timeout |
5000 |
ms |
autoApiTester.dryRun.default |
true |
Confirm non-GET requests |
autoApiTester.generator.mode |
seeded |
seeded: same data every run; random: new data every run |
autoApiTester.allowSelfSignedCertificates |
false |
Local HTTPS only |
autoApiTester.codeLens.enabled |
true |
|
autoApiTester.ai.enabled |
false |
Use Gemini when no schema is found |
autoApiTester.ai.model |
gemini-flash-latest |
|
autoApiTester.ai.mode |
fallback |
fallback or always |
autoApiTester.debug |
false |
Verbose log in the Auto API Tester output channel |
Seeded data
Each field's value comes from hash(file:METHOD:/full/path:fieldPath), so the same route always gets the same data, and adding a field doesn't change the others. Random mode records its seed in every result, so any run can be replayed exactly.
Limitations
- Express and NestJS. Fastify is a preview (no
register prefixes yet).
- Drizzle and Objection models are not read yet (Mongoose, Prisma, Sequelize and TypeORM are).
- NestJS
RouterModule path prefixes are not applied yet.
- Routes whose paths are built at runtime (
app.get(BASE + '/x')) are not detected.
- Response validation needs a TypeScript type on what the handler sends.
any payloads are shown but not validated.
Development
npm install
cp .env.example .env # optional: GEMINI_API_KEY for F5 development
npm run build # dist/extension.js, dist/engine.js (lazy-loaded), dist/webview.js
npm test # vitest: unit + fixture + end-to-end tests
npm run test:coverage
npm run lint && npm run typecheck && npm run format:check
npm run package # produces auto-api-tester-<version>.vsix
Press F5 to launch an Extension Development Host with test/fixtures/express-app open.
Layout: src/core holds the framework-independent pipeline (parser → route → analyzer → generator → request → response → testing) and has no VS Code imports. src/frameworks holds the adapters, src/commands, src/ui, src/config and src/storage hold the VS Code layer, and webview/ is the React result panel. Everything that needs ts-morph is bundled into engine.js and loaded on the first analysis, so the extension activates without loading the TypeScript compiler.
License
MIT © rAvi