Skip to content

Built for AI agents

  • No backend code needed. The URL of a running backend and its OpenAPI spec are enough.
  • Small context. The agent opens one short file per endpoint it calls, not the whole spec.
  • TypeScript as feedback. A wrong field doesn't compile, and the error points right at it.
How it works →

eberly

Typed end-to-end API tests for any backend with an OpenAPI 3 spec. Written by you or your AI agent.
Terminal window
npm i -D eberly

eberly reads your backend’s OpenAPI spec (JSON or YAML) and generates a typed TypeScript client for tests. Every test user gets their own state, hooks keep tokens and ids for you, and when the backend changes its contract, the tests stop compiling.

One assert checks the status and the body: exact values, schemas (Zod, Valibot, ArkType) and vitest matchers, all typed by the spec.

const world = new World()
const user = world.createUser()
await user.signUp() // register + log in
const body = { title: 'Hello', content: 'First post' }
const created = await user.posts.create({ body })
created.assert(201, { // typed status
title: 'Hello', // exact value
createdAt: z.iso.datetime(), // Zod, Valibot, ArkType…
authorId: expect.any(Number), // vitest / Jest matcher
})

Edit in the playground

Scenario: Alice creates a post, Bob tries to delete it and must get 403.

Before: bare fetch, tokens by hand, any everywhere.

const base = 'http://localhost:3000'
const json = { 'Content-Type': 'application/json' }
async function signUp(email: string) {
const body = JSON.stringify({ email, password: 'secret123' })
await fetch(`${base}/auth/register`, { method: 'POST', headers: json, body })
const login = await fetch(`${base}/auth/login`, { method: 'POST', headers: json, body })
const { accessToken } = await login.json() // any
return { ...json, Authorization: `Bearer ${accessToken}` }
}
const alice = await signUp('alice@example.com')
const bob = await signUp('bob@example.com')
const created = await fetch(`${base}/posts`, {
method: 'POST',
headers: alice,
body: JSON.stringify({ title: 'Hello', content: 'First post' }),
})
const post = await created.json() // any: `post.titel` compiles just fine
const res = await fetch(`${base}/posts/${post.id}`, { method: 'DELETE', headers: bob })
expect(res.status).toBe(403)
expect((await res.json()).message).toBe('Only the author can do this')

After: eberly (tests/readme.test.ts).

const world = new World()
const alice = world.createUser()
const bob = world.createUser()
await alice.signUp({ email: 'alice@example.com', password: 'secret123' })
await bob.signUp({ email: 'bob@example.com', password: 'secret123' })
const post = await alice.posts.create({ body: { title: 'Hello', content: 'First post' } })
const res = await bob.posts.remove({ path: { postId: post.assert(201).body.id } })
res.assert(403, { message: 'Only the author can do this' })

signUp is a scenario you write once in your userStore.ts. A hook (hooks.ts) adds the Bearer token to every request of that user. posts.create, posts.remove, the body and the statuses 201 / 403 are all checked by TypeScript.

An agent needs two things to write tests: the URL of a running backend and its OpenAPI spec. No access to the backend code.

  • Reads only what the test needs. eberly/api/INDEX.md lists the endpoints. The agent opens one short file per endpoint it calls, not the whole spec or the generated client.
  • TypeScript is the feedback loop. A wrong field or status doesn’t compile, and the error points at that exact field. The agent fixes it without guessing.
  • Skills included. npx eberly create installs Claude Code skills for setup and test writing.

What keeps a human’s head clear keeps an agent’s context small. More in Writing tests with AI agents and Motivation.

The playground runs right here: edit the spec and watch the test above go red where the contract changed. Or upload your own spec: it is processed in your browser and never leaves it.

Want the real thing? Open the NestJS example in StackBlitz: the backend starts in your browser, the client is generated from its spec and the tests go green in a couple of minutes. Edit a test and it reruns.

Open in StackBlitz

Typed client from your swagger

Request bodies, path params, response bodies and statuses come from the spec. The backend renames a field, and the test turns red before it even runs.

Many users, each with their own state

world.createUser() gives a user with their own token and variables, so “Bob can’t touch Alice’s post” is three lines.

Hooks keep tokens and ids for you

A before hook adds the token, an after hook saves the id of the created entity. Tests don’t pass them around.

Status with autocomplete, partial body check

res.assert(201, { title }) suggests the statuses declared in the spec, checks only the fields you pass (at any depth) and narrows res.body to the type of that status.

Transparent token refresh

A globalRetry hook refreshes the token on 401, and eberly replays the request: the test sees 200.

File upload by path

{ body: { file: './avatar.png' } }, and eberly builds the multipart form.


MIT · Formerly published as ebely.

Privacy-friendly analytics, no cookies.