The open-source TypeScript API testing framework — code-first, Git-native, OpenAPI-aware.
Write REST API tests in TypeScript. Validate responses against your OpenAPI spec automatically. Run from the CLI. Ship with confidence.
reqprobe is a lightweight, open-source API testing framework for TypeScript developers who want their tests to live in the codebase — not locked inside a GUI tool.
Most API testing tools use a graphical interface with proprietary scripting. reqprobe takes a different approach — tests are real TypeScript code: versioned in Git, reviewable in PRs, executable in any CI/CD pipeline with zero configuration.
| GUI Tools | reqprobe | |
|---|---|---|
| Lives in Git | ❌ Proprietary formats, not real code | ✅ .ts files — diff, blame, review |
| TypeScript | ❌ Proprietary scripting or custom DSL | ✅ Native, typed, full IDE support |
| OpenAPI contract testing | ❌ Manual, optional | ✅ Automatic per-request validation |
| CI/CD | ✅ npx reqprobe run — done |
|
| Schema-driven fuzzing | ❌ Not available | ✅ Built-in, from your OpenAPI spec |
| Import app types | ❌ Not possible | ✅ import type { User } from './src' |
| Cost | 💸 Free tiers limited, team features paid | ✅ Free, open-source |
If your API tests live in a GUI tool, they belong to that vendor — not your team. reqprobe puts them back in your codebase where they belong.
Most developers use a GUI-based API testing tool. These tools are useful for manual exploration — but they use proprietary scripting that is disconnected from your codebase. reqprobe tests are TypeScript, which unlocks four things no GUI tool can match:
// Your NestJS / Express app already defines this type:
import type { User } from '../src/users/user.entity';
import { test } from 'reqprobe/dsl';
test('POST /users — response is a valid User', async (ctx) => {
const res = await ctx.api.post('/users', { name: 'Alice', email: 'alice@example.com' });
// res.body is typed as User — IDE autocomplete, type checking, everything
const user = res.body as User;
ctx.expect(user.id).toBeTruthy();
ctx.expect(user.email).toBe('alice@example.com');
});If you rename a field in your app, TypeScript will flag the test at compile time — before CI runs, before the test even executes. No other API testing tool can do this.
Every method on ctx.api, every assertion on ctx.expect, every option in reqprobe.config.ts has complete type information. Your IDE autocompletes them, flags wrong arguments, and shows docs on hover. No tab-switching to documentation pages.
// shared test helpers live in your repo alongside the tests
import { createTestUser, cleanupTestUser } from '../helpers/test-fixtures';
import { API_ROUTES } from '../src/constants/routes';
test(`GET ${API_ROUTES.USERS} — returns paginated list`, async (ctx) => {
const user = await createTestUser();
const res = await ctx.api.get(API_ROUTES.USERS);
ctx.expect(res).toHaveStatus(200);
await cleanupTestUser(user.id);
});In GUI tools, you'd hard-code the route string and copy-paste setup/teardown scripts into every collection. Here it's just a normal import.
When your API changes — a renamed field, a removed endpoint, a changed status code — TypeScript catches the mismatch immediately when you run tsc. The feedback loop is seconds, not "CI failed after a 4-minute run."
| Feature | Description | |
|---|---|---|
| 📝 | TypeScript Native | Tests are .ts files with full IDE support, type checking, and refactoring. |
| 🛡️ | OpenAPI Contract Validation | Automatically validate every API response against your OpenAPI 3.x spec using Ajv. No extra assertions needed. |
| 🔐 | Auth Helpers | Bearer, Basic, API Key, and OAuth2 (client credentials) — configured once, applied to every request. Token caching included. |
| ⏳ | Async Polling | ctx.api.poll() — test job queues, webhooks, and background tasks with configurable interval and timeout. |
| 🏷️ | Tag Filtering | Tag tests with @smoke, @regression, @destructive — run subsets via --tag or --skip. |
| ⚡ | Parallel Execution | reqprobe run --workers 8 — run test files concurrently for faster CI pipelines. |
| 🔀 | Schema Fuzzing | ctx.fuzz.generate('/users', 'POST') generates realistic payloads from your OpenAPI spec. |
| 📊 | Rich Reports | Self-contained HTML, JSON, and JUnit XML reports. JUnit output works natively with Jenkins, GitLab CI, and Azure DevOps. |
| 🌱 | Git-Native | Tests are code — full diff history, PR reviews, and code coverage tooling just work. |
| ⚙️ | CI/CD Ready | Exits with code 1 on failure. Works with GitHub Actions, GitLab CI, Jenkins, and any CI runner. |
| 🏗️ | Monorepo Support | Per-package config files — each service owns its own tests and base URL. |
| 👁️ | Watch Mode | reqprobe run --watch — re-run tests on file save during development. |
| 📋 | Scaffold Generator | reqprobe generate --from openapi.json — generate typed test stubs from any OpenAPI spec. |
# npm (short form)
npm i reqprobe
# npm (full)
npm install reqprobe
# yarn
yarn add reqprobe
# pnpm
pnpm add reqprobeRequirements: Node.js 18 or higher.
npm i reqprobeRequirements: Node.js 18 or higher.
// reqprobe.config.ts
import type { Config } from 'reqprobe';
const config: Config = {
baseUrl: 'https://your-api.com',
timeout: 10_000,
auth: {
type: 'bearer',
token: process.env.API_TOKEN ?? '',
},
};
export default config;// tests/users.test.ts
import { test } from 'reqprobe/dsl';
test('GET /users — returns 200', async (ctx) => {
const res = await ctx.api.get('/users');
ctx.expect(res).toHaveStatus(200);
ctx.expect(res.body).toHaveProperty('data');
});
test('POST /users — creates a user @smoke', async (ctx) => {
const res = await ctx.api.post('/users', { name: 'Alice', email: 'alice@example.com' });
ctx.expect(res).toHaveStatus(201);
ctx.expect(res.body.name).toBe('Alice');
});npx reqprobe run "tests/**/*.test.ts"
# Run only smoke tests
npx reqprobe run --tag smoke
# Run 8 files in parallel
npx reqprobe run --workers 8Output:
❯ users.test.ts
✓ GET /users — returns 200 312ms
✓ POST /users — creates a user 189ms
────────────────────────────────────────
PASSED 501ms
────────────────────────────────────────
✓ Passed 2
✖ Failed 0
Total 2
────────────────────────────────────────
Exit code 1 on any failure — CI-ready with zero configuration.
Run this against the free PokéAPI — no auth, no setup:
// tests/pokeapi.test.ts
import { test } from 'reqprobe/dsl';
test('GET /pokemon/pikachu — returns correct name', async (ctx) => {
const res = await ctx.api.get('/pokemon/pikachu');
ctx.expect(res).toHaveStatus(200);
ctx.expect(res.body.name).toBe('pikachu');
});// reqprobe.config.ts
export default { baseUrl: 'https://pokeapi.co/api/v2', timeout: 10_000 };npx reqprobe run "tests/pokeapi.test.ts"// reqprobe.config.ts
import type { Config } from 'reqprobe';
const config: Config = {
baseUrl: 'https://api.yourservice.com',
timeout: 10_000,
// Auth applied automatically to every request — no per-test boilerplate
auth: {
type: 'bearer', // 'bearer' | 'basic' | 'api-key' | 'oauth2'
token: process.env.API_TOKEN ?? '',
},
// OpenAPI contract validation — optional, additive
openapi: {
specPath: './openapi.json',
strict: false, // true = fail if endpoint not in spec
},
// Reports — all optional
reporters: {
outDir: './reqprobe-reports',
html: true, // reqprobe-reports/report.html
json: true, // reqprobe-reports/report.json
junit: true, // reqprobe-reports/report.xml (Jenkins/GitLab/Azure)
},
};
export default config;reqprobe run --env staging # loads reqprobe.config.staging.ts
reqprobe run --env productionimport { test, beforeAll, afterAll } from 'reqprobe/dsl';
let authToken: string;
beforeAll(async () => {
const res = await fetch('https://api.example.com/auth/token', { method: 'POST' });
authToken = (await res.json()).token;
});
test('GET /users @smoke', async (ctx) => {
const res = await ctx.api.get('/users');
ctx.expect(res).toHaveStatus(200);
ctx.expect(res.body).toHaveProperty('data');
});
test('POST /users @regression', async (ctx) => {
const res = await ctx.api.post('/users', { name: 'Alice', email: 'alice@example.com' });
ctx.expect(res).toHaveStatus(201);
ctx.expect(res.body.id).toBeTruthy();
});import type { TestSuite } from 'reqprobe';
const suite: TestSuite = {
name: 'Auth API',
tests: [
{
name: 'POST /auth/login — returns token',
run: async (ctx) => {
const res = await ctx.api.post('/auth/login', {
email: 'admin@example.com',
password: 'secret',
});
ctx.expect(res).toHaveStatus(200);
ctx.expect(res.body.token).toBeTruthy();
},
},
],
};
export default suite;test('background export job completes', async (ctx) => {
const job = await ctx.api.post('/jobs', { type: 'export', format: 'csv' });
ctx.expect(job).toHaveStatus(202);
const result = await ctx.api.poll(`/jobs/${job.body.id}`, {
until: (res) => res.body.status === 'complete',
interval: 1000, // ms between checks
timeout: 30_000, // throws if never met
});
ctx.expect(result.body.downloadUrl).toBeTruthy();
});test('POST /users with generated payload', async (ctx) => {
const payload = ctx.fuzz.generate('/users', 'POST');
const res = await ctx.api.post('/users', payload);
ctx.expect(res).toHaveStatus(201);
});ctx.expect(res).toHaveStatus(200);
ctx.expect(res).toRespondWithin(500); // response time in ms
ctx.expect(res.body.name).toBe('Alice');
ctx.expect(res.body.items).toEqual([1, 2, 3]);
ctx.expect(res.body.message).toContain('success');
ctx.expect(res.body.token).toBeTruthy();
ctx.expect(res.body).toHaveProperty('id');Point reqprobe at your OpenAPI 3.x spec and every API response is automatically validated against its schema — no extra assertions needed in tests.
// reqprobe.config.ts
openapi: {
specPath: './openapi.json',
strict: false, // true = fail on endpoints missing from the spec
}// Every ctx.api call now validates the response automatically
test('GET /products/:id — response matches schema', async (ctx) => {
const res = await ctx.api.get('/products/42');
ctx.expect(res).toHaveStatus(200);
// reqprobe validates res.body against GET /products/{id} → 200 in the spec
// No extra assertion needed
});When a response doesn't match the schema, reqprobe gives a precise error:
✖ GET /products/42 — response matches schema (67ms)
├ [reqprobe/openapi] Response body failed schema validation:
├ • body.price: must be number
└ • body.stock: must have required property 'stock'
Supported:
- OpenAPI 3.x JSON specs
- Local
$refresolution - Path template matching (
/users/{id}) defaultresponse fallbackstrict: falsesilently skips missing schemas (safe for partial specs)
reporters: {
outDir: './reqprobe-reports',
html: true, // self-contained HTML — attach to PRs or upload as CI artifact
json: true, // machine-readable — dashboards, Slack bots, downstream tools
junit: true, // JUnit XML — Jenkins, GitLab CI test dashboard, Azure DevOps
}The HTML report is fully self-contained — open it in any browser with no server needed.
# .github/workflows/api-tests.yml
name: API Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Run API tests
run: npx reqprobe run "tests/**/*.test.ts" --workers 4
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
API_BASE_URL: ${{ vars.STAGING_URL }}
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: reqprobe-report
path: reqprobe-reports/api-tests:
image: node:20-alpine
script:
- npm ci
- npx reqprobe run "tests/**/*.test.ts"
artifacts:
when: always
reports:
junit: reqprobe-reports/report.xml # shows inline in GitLab MR
paths:
- reqprobe-reports/
expire_in: 7 daysstage('API Tests') {
steps {
sh 'npm ci'
sh 'npx reqprobe run "tests/**/*.test.ts"'
}
post {
always {
junit 'reqprobe-reports/report.xml'
}
}
}reqprobe reads the nearest reqprobe.config.ts. Each service owns its own config:
apps/
users-service/
reqprobe.config.ts # baseUrl: http://users-service
tests/
orders-service/
reqprobe.config.ts # baseUrl: http://orders-service
tests/
# From monorepo root
npx reqprobe run "apps/users-service/tests/**/*.test.ts"
# From within the service
cd apps/users-service && npx reqprobe run "tests/**/*.test.ts"See ROADMAP.md for the full backlog with code examples and architecture notes.
| Feature | Notes |
|---|---|
| ✅ TypeScript-native test runner | test() DSL + TestSuite object pattern |
| ✅ Lifecycle hooks | beforeAll / beforeEach / afterEach / afterAll |
| ✅ Full assertion library | toBe, toEqual, toContain, toHaveStatus, toRespondWithin, … |
| ✅ OpenAPI 3.x contract validation | Automatic per-request schema check via Ajv — no extra assertions needed |
| ✅ Schema-driven fuzzing | ctx.fuzz.generate('/users', 'POST') + reqprobe fuzz CLI |
| ✅ Auth helpers | bearer, basic, api-key, oauth2 — configured once, applied everywhere. OAuth2 token cached automatically. |
| ✅ Async polling | ctx.api.poll() for job queues, webhooks, and background tasks |
| ✅ Tag filtering | test('name @smoke', …) → reqprobe run --tag smoke / --skip destructive |
| ✅ Parallel execution | reqprobe run --workers 8 — concurrent file execution with isolated registries |
| ✅ HTML + JSON + JUnit XML reports | JUnit for Jenkins, GitLab CI, Azure DevOps, TeamCity |
| ✅ Watch mode | reqprobe run --watch — re-runs on file save |
| ✅ Scaffold generator | reqprobe generate --from openapi.json — typed test stubs from spec |
| ✅ CI exit codes | Exits 1 on failure — zero config required |
✅ .env support + environment profiles |
Per-environment config files (reqprobe.config.staging.ts) |
| ✅ Retry logic | Per-request retry: { times, delay, on } + global config — retries on status codes or network errors |
| ✅ Richer failure diagnostics | Full HTTP exchange on failure — request, masked headers, response body, assertion diff |
Multipart file upload · Cookie jar · ctx.store cross-request state · Multi-environment config block
GraphQL support · Mock server from spec · Snapshot testing · OpenAPI spec diff · WebSocket & SSE testing · Plugin API · Schema-driven load testing
Spec coverage report · OpenTelemetry tracing · Consumer-driven contract testing · Chaos/fault injection · gRPC support · Multi-region performance testing (open-source, self-hostable)
Self-hostable team dashboard · AI test generation from spec · AI anomaly detection · Enterprise SSO / RBAC
Contributions are welcome. reqprobe is intentionally small — keep PRs focused.
git clone https://github.com/shashi089/reqprobe.git
cd reqprobe
npm install
npm run buildConventions:
- No new runtime dependencies without discussion — current footprint is intentionally minimal (
ajv,commander,dotenv,fast-glob,picocolors,tsx) - Single responsibility — each module in
src/has one job - No circular imports — dependency graph is strictly one-way
- TypeScript strict mode —
tscmust exit 0 before any PR merges
Submitting a PR:
- Fork and create a feature branch
- Make your change + add or update examples in
examples/ - Run
npm run build— must exit 0 - Open a PR with a clear description of what and why
Reporting bugs — open an issue with:
- reqprobe version (
npx reqprobe --version) - Node version (
node --version) - Minimal reproduction (test file + config)
- Actual vs expected output
MIT © Shashidhar Naik