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.
Unlike Postman, Bruno, Insomnia, or Hoppscotch, reqprobe treats API tests as real TypeScript code: versioned in Git, reviewable in PRs, executable in any CI/CD pipeline with zero configuration.
| GUI Tools (Postman, Insomnia, Bruno) | reqprobe | |
|---|---|---|
| Lives in Git | ❌ JSON exports, not real code | ✅ .ts files — diff, blame, review |
| TypeScript | ❌ Proprietary scripting | ✅ 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 |
| Cost | 💸 Subscription required for team features | ✅ Free, open-source, self-hostable |
If your API tests live in a GUI, they belong to a vendor — not your team. reqprobe puts them back in your codebase where they belong.
Most developers use Postman or Bruno for API testing. Both are great tools — 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 'req-probe/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 Postman, 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
npm install req-probe
# yarn
yarn add req-probe
# pnpm
pnpm add req-probeRequirements: Node.js 18 or higher.
// reqprobe.config.ts
import type { Config } from 'req-probe';
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 'req-probe/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 'req-probe/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 'req-probe';
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 'req-probe/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 'req-probe';
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 prioritised backlog.
| 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 |
| ✅ Schema-driven fuzzing | ctx.fuzz.generate('/users', 'POST') + reqprobe fuzz CLI |
| ✅ Auth helpers | bearer, basic, api-key, oauth2 — configured once, applied everywhere |
| ✅ Async polling | ctx.api.poll() for job queues, webhooks, and background tasks |
| ✅ Tag filtering | test('name @smoke', …) → reqprobe run --tag smoke |
| ✅ Parallel execution | reqprobe run --workers 8 |
| ✅ HTML + JSON + JUnit XML reports | JUnit for Jenkins, GitLab CI, Azure DevOps |
| ✅ Watch mode | reqprobe run --watch |
| ✅ Scaffold generator | reqprobe generate --from openapi.json |
| ✅ CI exit codes | Exits 1 on failure — zero config required |
✅ .env support + environment profiles |
Per-environment config files |
Up next — ROADMAP.md
GraphQL support · Mock server from spec · Snapshot testing · OpenAPI spec diff · Load testing · Multi-region performance testing
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