endpoint-tester
v0.4.3
Published
Auto-discover API endpoints and generate comprehensive test suites
Maintainers
Readme
endpoint-tester
Auto-discover API endpoints in your source code and generate ready-to-run test suites.
npm · GitHub · Dev.to Article
Demo
A full scan + generate cycle on a real Express app in ~5 seconds. See the Quick Demo section for the exact terminal output.
The problem
Every API project needs endpoint tests. Writing them is tedious, repetitive, and error-prone: you copy-paste test files, update paths, remember which params go where, and hope you didn't miss a route. When the codebase changes, the tests fall behind.
The solution
endpoint-tester scans your source code, discovers every API endpoint automatically, and generates test suites that are ready to run. Point it at your project, get a complete test file in seconds.
Source code in --> [endpoint-tester] --> Test suite out
Express Vitest / Jest
Fastify Pytest
Koa
NestJS
FastAPI
Flask
Django
Spring BootHow It Works
┌─────────────────────────────────────────────────────────────────┐
│ Your Source Code │
│ src/routes/users.ts src/routes/orders.ts src/app.ts │
└───────────────────────────────┬─────────────────────────────────┘
│
┌───────────▼───────────┐
│ Framework Detection │
│ (package.json / AST) │
│ express, fastapi, ... │
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ Adapter / Parser │
│ Reads route patterns │
│ Extracts params/body │
└───────────┬───────────┘
│
┌────────────────▼────────────────┐
│ Endpoint Registry │
│ GET /users │
│ POST /users body: {...} │
│ GET /users/:id param: id │
│ PUT /users/:id param: id │
│ DELETE /users/:id param: id │
└────────────────┬────────────────┘
│
┌────────────────────▼──────────────────────┐
│ Test Generator │
│ - status code assertions (GET→200, POST→201) │
│ - auth header tests (Bearer token) │
│ - error tests (missing body → 4xx) │
│ - boundary value tests (empty, negative ID) │
└────────────────────┬──────────────────────┘
│
┌─────────────────────▼──────────────────────┐
│ Output │
│ tests/api.test.ts openapi.yaml stdout │
└────────────────────────────────────────────┘Quick Demo
A real Express.js app, from zero to a complete test suite in under 60 seconds.
The app (src/app.ts):
import express from 'express';
import { Router } from 'express';
const app = express();
const router = Router();
router.get('/users', listUsers);
router.post('/users', createUser);
router.get('/users/:id', getUser);
router.put('/users/:id', updateUser);
router.delete('/users/:id', deleteUser);
router.get('/users/:id/orders', getUserOrders);
app.use('/api', router);
app.listen(3000);Step 1 — Install
npm install -g endpoint-tester
# or use without installing:
npx endpoint-tester scan ./srcStep 2 — Scan (auto-detects Express)
$ endpoint-tester scan ./src
Auto-detected framework: express (high confidence)
Scanning ./src for express endpoints...
Found 6 endpoint(s):
GET /api/users
POST /api/users
GET /api/users/:id [params: id]
PUT /api/users/:id [params: id]
DELETE /api/users/:id [params: id]
GET /api/users/:id/orders [params: id]Step 3 — Generate tests
$ endpoint-tester generate ./src --format vitest --output ./tests/api.test.ts
Generated 6 test cases → ./tests/api.test.tsThe output (tests/api.test.ts) — ready to run with npx vitest:
import { describe, it, expect } from 'vitest';
const BASE_URL = 'http://localhost:3000';
const AUTH_TOKEN = process.env.TEST_AUTH_TOKEN ?? 'test-token';
describe('GET /api/users', () => {
it('returns 200', async () => {
const res = await fetch(`${BASE_URL}/api/users`);
expect(res.status).toBe(200);
});
it('returns 401 without auth', async () => {
const res = await fetch(`${BASE_URL}/api/users`, {
headers: { Authorization: `Bearer invalid` },
});
expect(res.status).toBeOneOf([401, 403]);
});
});
describe('POST /api/users', () => {
it('returns 201 with body', async () => {
const res = await fetch(`${BASE_URL}/api/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${AUTH_TOKEN}`,
},
body: JSON.stringify({}),
});
expect(res.status).toBe(201);
});
it('returns 4xx with missing body', async () => {
const res = await fetch(`${BASE_URL}/api/users`, { method: 'POST' });
expect(res.status).toBeGreaterThanOrEqual(400);
});
});
describe('GET /api/users/:id', () => {
it('returns 200 for valid id', async () => {
const res = await fetch(`${BASE_URL}/api/users/1`);
expect(res.status).toBe(200);
});
it('returns 404 for nonexistent id', async () => {
const res = await fetch(`${BASE_URL}/api/users/999999`);
expect(res.status).toBe(404);
});
it('returns 4xx for empty id', async () => {
const res = await fetch(`${BASE_URL}/api/users/`);
expect(res.status).toBeGreaterThanOrEqual(400);
});
});
// ... DELETE, PUT, and nested routes follow the same patternStep 4 — Run (against your running server)
$ npx vitest run tests/api.test.ts
PASS tests/api.test.ts (1.2s)
GET /api/users
✓ returns 200
✓ returns 401 without auth
POST /api/users
✓ returns 201 with body
✓ returns 4xx with missing body
GET /api/users/:id
✓ returns 200 for valid id
✓ returns 404 for nonexistent id
✓ returns 4xx for empty idBonus — export OpenAPI spec
$ endpoint-tester generate ./src --format openapi --output openapi.yaml
openapi: "3.1.0"
info:
title: API
version: "1.0.0"
paths:
/api/users:
get:
summary: GET /api/users
responses:
"200": { description: OK }
/api/users/{id}:
get:
parameters:
- name: id
in: path
required: true
schema: { type: string }
...Feed this to Swagger UI, Schemathesis, or openapi-generator — no manual annotations needed.
Postman Collection export
$ endpoint-tester generate ./src --format postman --output postman-collection.json
Generated Postman Collection v2.1.0 -> postman-collection.jsonImport the file straight into Postman (or Insomnia — it also reads v2.1). Every request comes with a shared {{baseUrl}} variable, a placeholder Authorization: Bearer {{authToken}} header, and — for POST/PUT/PATCH — a raw JSON body seeded from the inferred fields.
Bonus — detect drift against an existing OpenAPI spec
Already ship a hand-maintained openapi.json? Point endpoint-tester diff at it and get a fail-loud drift check in one command — perfect for CI:
$ endpoint-tester diff ./src ./openapi.json
Scanning ./src for express endpoints...
Found 5 discovered endpoint(s).
Loading OpenAPI spec from ./openapi.json...
Drift detected between OpenAPI spec and source code (3 matched).
In spec, but not in source (1):
- DELETE /users/{id}
In source, but not in spec (1):
+ PATCH /products/{id}Exits non-zero when drift is found, so it drops into any CI pipeline as a hard gate. Add --json for machine-readable output. Path placeholders normalise across frameworks: Express :id, Flask <int:id>, and OpenAPI {id} all compare equal.
Features
- Auto-detection -- Detects your framework automatically from package.json, requirements.txt, pom.xml, or source imports. No config needed.
- 13 framework adapters -- Express.js, Fastify, Koa, NestJS, Hono, FastAPI, Flask, Django, Spring Boot, Gin, Echo, Chi, net/http. Extensible for any framework via the Adapter interface.
- 3 test formats -- Vitest, Jest, Pytest. Generated tests include status code assertions, auth header tests, error response tests, and boundary value tests.
- OpenAPI 3.1 output -- Emit a spec straight from your source code (
--format openapi, JSON or YAML). Feed it to Swagger UI, Schemathesis, Dredd, Apidog, oropenapi-generator— no manual annotations, zero runtime dependencies. - Postman Collection v2.1 -- Emit a ready-to-import collection (
--format postman), with{{baseUrl}}/{{authToken}}variables, body skeletons, and per-request path + query parameter descriptions. - OpenAPI drift detection -- Already have a hand-maintained
openapi.json? Runendpoint-tester diff <src> <spec>to reconcile discovered routes against the spec and exit non-zero on drift. Drop it into CI to block PRs that add or delete endpoints without updating the spec. - Smart route parsing -- Handles router prefixes, middleware chains,
app.route()chaining, multi-line decorators, class-level annotations, Blueprints, and more. - Zero config -- Works out of the box. One command, one output.
Install
Requirements: Node.js >= 20
# Install globally (recommended)
npm install -g endpoint-tester
# Or use without installing (always pulls the latest version)
npx endpoint-tester scan ./srcVerify the installation:
endpoint-tester --version
# 0.3.0Quick start
Point endpoint-tester at your source directory — it auto-detects the framework.
# 1. Scan for endpoints (auto-detects framework from package.json / requirements.txt / pom.xml)
endpoint-tester scan ./src
# 2. Generate a test suite
endpoint-tester generate ./src --format vitest --output ./tests/api.test.ts
# 3. Run the generated tests (against your running server)
npx vitest run ./tests/api.test.tsFramework-specific examples:
# Express.js — auto-detected from package.json
endpoint-tester scan ./src
# FastAPI — auto-detected from requirements.txt
endpoint-tester scan ./app
# Force a specific framework
endpoint-tester scan ./src --framework nestjs
endpoint-tester scan ./src --framework spring
# Generate a Pytest suite for FastAPI
endpoint-tester generate ./app --framework fastapi --format pytest --output ./tests/test_api.py
# Generate an OpenAPI 3.1 spec (no server required)
endpoint-tester generate ./src --format openapi --output openapi.yamlWorking examples you can clone and run right now:
# Express.js example
examples/express-sample/ # see examples/express-sample/README.md
# FastAPI example
examples/fastapi-sample/ # see examples/fastapi-sample/README.md
# Go examples (Gin, Echo, Chi, net/http)
examples/go-sample/ # see examples/go-sample/README.mdExample output
Given an Express app:
// src/routes/users.ts
router.get('/users', listUsers);
router.post('/users', createUser);
router.get('/users/:id', getUser);
router.put('/users/:id', updateUser);
router.delete('/users/:id', deleteUser);Running endpoint-tester scan ./src outputs:
Auto-detected framework: express (high confidence)
Scanning ./src for express endpoints...
Found 5 endpoint(s):
GET /users
POST /users
GET /users/:id [params: id]
PUT /users/:id [params: id]
DELETE /users/:id [params: id]Running endpoint-tester generate ./src --format vitest generates a complete test file with:
- Success tests with method-specific status codes (POST expects 201, DELETE expects 204, etc.)
- Auth header tests (Bearer token)
- Error response tests (missing body returns 4xx)
- Boundary value tests for path parameters (empty, negative, nonexistent)
CLI reference
scan / generate
| Option | Description | Default |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
| --framework / -f | Framework adapter (express, fastapi, spring, django, flask, fastify, koa, nestjs, hono, gin, echo, chi, nethttp). Auto-detected if omitted. | auto-detect |
| --output / -o | Output path -- directory or file path | ./generated-tests |
| --format | Output format (vitest, jest, pytest, go, openapi, postman) | vitest |
| --base-url | Base URL for test requests | http://localhost:3000 |
| --watch / -w | Watch for source file changes and re-scan/regenerate automatically | off |
| --exclude / -e | Glob patterns to exclude (repeatable) | none |
| --verbose / -v | Show source file and line number for each endpoint (scan only) | off |
ci
Run in CI to guard against accidental endpoint deletion:
# First run saves a baseline (auto-created if missing)
endpoint-tester ci ./src
# Subsequent CI runs — exits 1 if fewer endpoints than baseline
endpoint-tester ci ./src
# After an intentional endpoint removal, refresh the baseline
endpoint-tester ci ./src --update-baseline
# Custom baseline file path (default: .endpoint-tester-baseline.json)
endpoint-tester ci ./src --baseline-file ci/baseline.jsonExit codes: 0 = pass (count same or higher), 1 = fail (count dropped below baseline).
diff
Reconcile discovered endpoints against an OpenAPI 3.x spec (JSON) and fail fast on drift — ideal as a CI gate on repos where the spec is the source of truth:
# Compare discovered routes against a checked-in spec
endpoint-tester diff ./src ./openapi.json
# Machine-readable output (drift report as JSON)
endpoint-tester diff ./src ./openapi.json --json
# Same, force framework
endpoint-tester diff ./src ./openapi.json --framework fastapiExit codes: 0 = in sync, 1 = drift detected (missing endpoints on either side).
Path placeholders normalise across frameworks and specs: Express :id, Flask <int:id>, and OpenAPI {id} all compare equal.
from
Generate a test scaffold directly from an OpenAPI/Swagger spec — no source scanning required. Ideal when the API contract is your source of truth (public APIs, spec-first services, gateways).
Supports OpenAPI 3.x and Swagger 2.0, JSON or YAML, local path or http(s) URL:
# From a local JSON spec (default: vitest tests written to ./generated-tests/)
endpoint-tester from ./openapi.json
# From a YAML spec, custom output and format
endpoint-tester from ./openapi.yaml --output ./tests --format pytest
# From a remote spec URL
endpoint-tester from https://petstore3.swagger.io/api/v3/openapi.json --output ./tests
# Re-emit as a Postman collection
endpoint-tester from ./openapi.json --format postman --output ./postman.json
# Override the base path applied to every imported endpoint
endpoint-tester from ./openapi.yaml --base-path /api/v2| Option | Description | Default |
| ------------- | ------------------------------------------------------------------ | ----------------------- |
| --output | Output path — directory or file | ./generated-tests |
| --format | Output format (vitest, jest, pytest, go, openapi, postman) | vitest |
| --base-url | Base URL for test requests | http://localhost:3000 |
| --base-path | Prepend a base path to every imported endpoint (overrides Swagger 2 basePath) | none |
Exit codes: 0 on success, 1 when the spec cannot be loaded or parsed, --format/--base-url are invalid, or the spec fails OpenAPI validation.
GitHub Action
For CI on GitHub, you don't need to install anything — a composite action lives at the repo root (action.yml). It runs endpoint-tester ci and fails the workflow if any endpoint is removed since the last baseline. Minimal example:
# .github/workflows/endpoints.yml
name: endpoint drift guard
on:
pull_request:
branches: [main]
jobs:
guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: leopechnicki/[email protected]
with:
directory: ./src
# framework: express # omit to auto-detect
# baseline-file: .endpoint-tester-baseline.json
# update-baseline: 'false' # 'true' to refresh baselineThe action installs the latest published endpoint-tester (or the version you pin in with.version:), runs endpoint-tester ci, and exposes three outputs:
| Output | Description |
| ---------------- | ------------------------------------------------ |
| endpoint-count | Number of endpoints discovered on this run |
| baseline-count | Number recorded in the baseline (or 0 if none) |
| drift | ok / added / removed / no-baseline |
Use them for downstream conditional steps (e.g. only notify Slack when drift == 'removed').
Config file
Instead of repeating CLI flags on every run, you can set defaults in a .endpointtesterrc (or .endpointtesterrc.json) file in the root of the project you are scanning:
{
"framework": "express",
"outputDir": "./tests/generated",
"testRunner": "vitest",
"baseUrl": "http://localhost:3000",
"exclude": ["legacy/**", "migrations/**"]
}The loader searches for the config in this order (first hit wins):
- The scanned directory (e.g.
endpoint-tester generate ./srclooks inside./src). - The current working directory — where you actually ran the command. This is the case most users hit: rc file at the project root,
endpoint-tester generate ./src. - Parent directories of the scanned directory, walking up to the filesystem root (same pattern as ESLint/Prettier).
CLI flags always override config file values. Supported fields:
| Field | Type | Description |
| ------------ | -------- | -------------------------------------------------------------- |
| framework | string | Override auto-detection (express, fastapi, nestjs, etc.) |
| outputDir | string | Default output directory for generated tests |
| testRunner | string | Default format (vitest, jest, pytest, go, openapi) |
| baseUrl | string | Default base URL used in generated tests |
| exclude | string[] | Glob patterns to exclude from scanning |
| include | string[] | Additional glob patterns to include |
Watch mode
Re-scan and regenerate every time a source file changes:
# Watch and rescan
endpoint-tester scan ./src --watch
# Watch and regenerate tests
endpoint-tester generate ./src --watch --output ./tests/api.test.tsWatches .ts, .js, .py, .go, .java, .kt, .rs files. Changes are debounced (300 ms) to avoid spurious re-runs during saves. Press Ctrl+C to stop.
Supported frameworks
| Framework | Patterns detected |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Express.js | app.get(), router.post(), app.route().get().post(), route params, router prefixes via app.use() and router.use(), middleware chains |
| Hono | app.get(), app.post(), app.route() groups, app.use() mounts, :param path params, c.req.query() inference, body inference from c.req.json() |
| Fastify | fastify.get(), fastify.route({ method, url, handler }), shorthand method registrations |
| Koa | @koa/router with router.get() / router.post(), route params, router.prefix() |
| NestJS | @Controller('prefix') + method decorators (@Get, @Post, ...), @Param, @Query, @Body DTO inference |
| FastAPI | @app.get(), @router.post(), APIRouter prefixes, {param} parameters, multi-line decorators with kwargs |
| Flask | @app.route() with methods list, @app.get() shorthand, Blueprint url_prefix, typed parameters (<int:id>) |
| Django | path(), re_path(), typed parameters (<int:pk>), regex named groups |
| Spring Boot | @GetMapping, @PostMapping, @RequestMapping (both argument orderings), class-level @RequestMapping prefix, @PathVariable, multiline annotations, Kotlin fun syntax |
| Gin | r.GET(), r.POST(), router.Group() prefixes, route params (:id), gin.Default() and gin.New() |
| Echo | e.GET(), e.POST(), e.Group() prefixes, route params (:id), echo.New() |
| Chi | r.Get(), r.Post(), r.Route(), r.Mount() prefixes, route params ({id}) |
| net/http | http.HandleFunc(), mux.HandleFunc(), http.Handle(), route params (custom patterns) |
Test formats
| | Vitest | Jest | Pytest |
| -------------- | ----------------------------------- | ------------------------ | ------------------------------------ |
| Imports | import { describe, it, expect } | Uses globals (no import) | import requests |
| File | .ts | .ts | .py |
| Assertions | expect(response.status).toBe(201) | Same | assert response.status_code == 201 |
All formats generate:
- Method-specific status code assertions (GET -> 200, POST -> 201, DELETE -> 204)
- Auth header tests with Bearer token
- Error response tests for body-accepting endpoints
- Boundary value tests for path parameters
Programmatic API
import {
Scanner,
TestGenerator,
getAdapter,
detectFramework,
} from 'endpoint-tester';
// Auto-detect the framework
const detected = await detectFramework('./src');
if (!detected) throw new Error('Could not detect framework');
const adapter = getAdapter(detected.framework);
// Scan for endpoints
const scanner = new Scanner(adapter);
const endpoints = await scanner.scan({
directory: './src',
framework: detected.framework,
});
// Generate tests
const generator = new TestGenerator();
const tests = generator.generate({
endpoints,
output: './tests',
format: 'vitest',
baseUrl: 'http://localhost:3000',
});Custom adapters
Implement the Adapter interface to add support for any framework:
import { Adapter, Endpoint, Framework, registerAdapter } from 'endpoint-tester';
class HonoAdapter implements Adapter {
framework = 'hono' as Framework;
fileExtensions = ['.ts', '.js'];
parse(source: string, filePath?: string): Endpoint[] {
// Your parsing logic here
return [];
}
}
registerAdapter(new HonoAdapter());Comparison with alternatives
vs. Schemathesis, Bruno, and Optic
The key distinction: most tools go spec → tests. endpoint-tester goes source code → spec.
| | endpoint-tester | Schemathesis | Bruno | Optic |
| ------------------- | -------------------------- | ------------------------ | -------------------------- | ------------------------------ |
| Starting point | Source code | OpenAPI/GraphQL spec | Existing API collection | OpenAPI spec or traffic |
| Spec required? | No — generates it | Yes | Yes (or import) | Yes |
| Test framework | Vitest, Jest, Pytest, Go | pytest (property-based) | Bruno runner | Optic CI |
| Approach | Static analysis of routes | Fuzzing + property tests | Manual collection + run | Diff / contract testing |
| Framework-aware | 13 built-in adapters | Spec-agnostic | Spec-agnostic | Spec-agnostic |
| Setup time | Zero — point at source dir | Write/import spec first | Import or write collection | Import spec or capture traffic |
| CI guard | Built-in (ci command) | Via pytest integration | Bruno CLI | Optic CI action |
| Watch mode | Built-in (--watch) | No | No | No |
| OpenAPI export | Yes (3.1, JSON + YAML) | Consumes, not generates | Consumes | Generates from traffic |
| Price | Free / open-source | Free / open-source | Free / open-source | Free tier + paid |
| Language | TypeScript (Node.js) | Python | Any (CLI) | TypeScript |
When to use each:
- endpoint-tester — You have source code but no spec yet. Start here: get tests and a spec in one step.
- Schemathesis — You have a spec and want property-based fuzzing to find edge cases.
- Bruno — You prefer a Git-friendly Postman replacement for manual API exploration.
- Optic — You want to diff two spec versions or detect breaking changes in CI.
vs. writing tests manually or Postman
| | endpoint-tester | Writing tests manually | Postman export | | ---------------------- | ---------------- | ---------------------- | --------------------- | | Setup time | 0 (auto-detects) | N/A | Import collection | | Keeps up with code | Re-scan anytime | Manual updates | Re-export | | Boundary tests | Automatic | Write each one | Manual | | Auth tests | Automatic | Write each one | Configure per request | | Multi-framework | 13 built-in | N/A | Framework-agnostic | | CI friendly | CLI output | Already in repo | Needs Newman |
Development
Prerequisites
- Node.js >= 20
- npm
Setup
git clone https://github.com/leopechnicki/endpoint-tester.git
cd endpoint-tester
npm installCommands
npm run build # Compile TypeScript to dist/
npm test # Run tests with vitest
npm run lint # Lint with ESLint
npm run dev # Watch mode (tsc --watch)Library usage (programmatic API)
Install as a dependency:
npm install endpoint-testerImport types and classes:
import {
Scanner,
TestGenerator,
getAdapter,
registerAdapter,
detectFramework,
Framework,
type Adapter,
type Endpoint,
type EndpointParam,
type EndpointBody,
type HttpMethod,
type ScanOptions,
type GenerateOptions,
} from 'endpoint-tester';Contributing
Contributions are welcome. Areas with the most impact:
- New framework adapters (Hono, Actix, Laravel, etc.)
- Smarter body inference from type annotations
- Watch mode for continuous test generation
Commit messages follow Conventional Commits
(feat:, fix:, chore:, ...). Releases are cut automatically by
release-please
based on the commit history.
License
MIT
