Bruno gets the big thing right: your API collections are plain text files that live in your repo, work offline, and diff like code โ no mandatory cloud account, no proprietary sync. We're not an alternative to it; Bruno is the client half. This guide is the other half: a live, hosted API worth pointing Bruno at โ one you can mutate, break on purpose, and slow down. Two paths below: generate a complete collection from a live OpenAPI spec with a single bru import, or hand-write a small .bru suite with assertions that passes verbatim โ chaining, a JWT auth flow, forced error states, latency assertions, and CI exit codes.
Everything on this page was run with Bru CLI 4.0.0 (installed fresh from npm) against production before publishing; outputs shown are real.
Every Mockbird project publishes a live OpenAPI 3.0 spec at /m/<project>/openapi.json, and Bruno's CLI imports OpenAPI directly โ straight from the URL:
npm install -g @usebruno/cli
bru import openapi \
-s https://mockbird.mockbird.workers.dev/m/demo/openapi.json \
-o demo-collection
That one command produced this (real output tree):
demo-collection/
customers/ List ยท Get one ยท Create ยท Replace ยท Update fields ยท Delete
orders/ (same six requests)
products/ (same six)
reviews/ (same six)
environments/Environment 1.yml โ baseUrl already set for you
Every request arrives pre-documented: pagination (page/limit), sorting, full-text search, per-field filters with _gte/_lte/_ne/_like operator suffixes, and the simulation params (mock_status, mock_delay, mock_chaos, mock_seqโฆ) all appear as described query params โ most pre-disabled so you can toggle them per run. Then:
cd demo-collection
bru run "products/List products.yml" --env "Environment 1"
# โ 1 (1 Passed), 392 ms
The desktop app has the same importer (Import โ OpenAPI, paste the spec URL), so the GUI crowd gets the identical collection without touching a terminal. The demo API is real and stateful: Create a product actually persists, and Get one reads it back. (The shared demo reseeds daily; for anything you want to keep, use your own project and import its spec instead.)
If you prefer writing .bru files โ the format that makes Bruno reviewable in a pull request โ here is a complete four-request collection with assertions. Make a folder, drop these in, run it. It passes as-is.
bruno.json:
{ "version": "1", "name": "mockbird-smoke", "type": "collection" }
list-products.bru โ status, shape, and a real pagination header:
meta {
name: List products
type: http
seq: 1
}
get {
url: https://mockbird.mockbird.workers.dev/m/demo/products?limit=5
}
assert {
res.status: eq 200
res.body[0].id: isDefined
res.headers['x-total-count']: isDefined
}
error-state.bru โ force a 503 with a query param and assert on it (this is how you develop your client's error branch against a real 503, not a stubbed one):
meta {
name: Forced 503 error state
type: http
seq: 2
}
get {
url: https://mockbird.mockbird.workers.dev/m/demo/products?mock_status=503
}
assert {
res.status: eq 503
}
create-order.bru โ a write that persists, saving the new id for the next request:
meta {
name: Create an order
type: http
seq: 3
}
post {
url: https://mockbird.mockbird.workers.dev/m/demo/orders
body: json
}
body:json {
{ "status": "pending", "total": 42.5 }
}
assert {
res.status: eq 201
res.body.id: isDefined
}
script:post-response {
bru.setVar("orderId", res.body.id);
}
get-order-back.bru โ proof the write stuck (JSONPlaceholder-style fake writes can't pass this one):
meta {
name: Read the order back
type: http
seq: 4
}
get {
url: https://mockbird.mockbird.workers.dev/m/demo/orders/{{orderId}}
}
assert {
res.status: eq 200
res.body.total: eq 42.5
}
bru run
# get-order-back (200 OK) - 175 ms
# โ res.status: eq 200
# โ res.body.total: eq 42.5
# Requests: 4 (4 Passed) ยท Assertions: 8/8
Bruno's auth blocks and post-response scripts are exactly the muscle you need for real APIs โ the demo ships a mock auth endpoint that issues real signed JWTs to practice against. Any email/password logs in:
meta {
name: Mock login returns a real JWT
type: http
seq: 5
}
post {
url: https://mockbird.mockbird.workers.dev/m/demo/auth/login
body: json
}
body:json {
{ "email": "test@example.com", "password": "anything" }
}
assert {
res.status: eq 200
res.body.token: isDefined
}
script:post-response {
bru.setVar("token", res.body.token);
}
meta {
name: /auth/me with the token
type: http
seq: 6
}
get {
url: https://mockbird.mockbird.workers.dev/m/demo/auth/me
auth: bearer
}
auth:bearer {
token: {{token}}
}
assert {
res.status: eq 200
res.body.user.email: eq test@example.com
}
Both pass verbatim. On your own project you can flip the whole API to protected mode and every request 401s without a Bearer token โ the full auth-wiring drill, end to end.
?mock_delay=1500 makes the endpoint genuinely take 1.5 s โ so res.responseTime assertions have something real to measure:
get {
url: https://mockbird.mockbird.workers.dev/m/demo/products/1?mock_delay=1500
}
assert {
res.status: eq 200
res.responseTime: gte 1500
}
Passes (it took ~1.6 s over the wire). For flakiness drills there's ?mock_chaos=0.3 (fail 30% of requests with random 5xx/429) and ?mock_seq=503,503,200 (deterministic: first two requests 503, then 200 โ perfect for testing retry logic in a Bruno script). Full param list in the docs and in testing loading and error states.
bru run exits 1 when any assertion fails (verified: a forced mock_status=500 against an eq 200 assert exits 1), so wiring the suite into CI is one step:
# .github/workflows/api-smoke.yml
name: api-smoke
on: [push]
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install -g @usebruno/cli
- run: bru run --env "Environment 1"
working-directory: ./collection
Because the mock is a hosted URL, the CI runner needs zero services, containers, or seed scripts โ the backend just exists. For an isolated backend per CI run (create project โ run suite โ delete project), the pattern is in mock APIs in GitHub Actions โ it slots straight into a Bruno job.
Repeatable runs on a long-lived project: the backend is stateful, so a collection containing DELETE requests really deletes โ a second run 404s on the records the first run removed. If you'd rather keep one project than create/delete per run, save a snapshot once and restore it before each run; every run then starts from identical data. We verified exactly this loop against production: a full generated-collection run all green โ rerun 404s (state persisted, as designed) โ restore โ all green again.
# once:
curl -X POST https://mockbird.mockbird.workers.dev/api/projects/<id>/snapshots \
-H "x-admin-key: $ADMIN_KEY" -H 'content-type: application/json' -d '{"name":"baseline"}'
# before each CI run:
curl -X POST https://mockbird.mockbird.workers.dev/api/projects/<id>/snapshots/baseline/restore \
-H "x-admin-key: $ADMIN_KEY"
bru run --env "Environment 1"
The demo is shared and reseeds daily. Your own project takes one curl (or one click), no signup:
curl -s -X POST https://mockbird.mockbird.workers.dev/api/projects \
-H "content-type: application/json" \
-d '{"preset":"ecommerce"}'
# โ {"id":"abc123xyz9", "adminKey":"โฆ", ...}
bru import openapi \
-s https://mockbird.mockbird.workers.dev/m/abc123xyz9/openapi.json \
-o my-collection
Same one-command collection, but the data is yours: define your own resources and field types, import your existing OpenAPI spec, a db.json, or a CSV โ and the generated Bruno collection follows your schema. Projects also export a Postman Collection v2.1 at /m/<project>/postman.json if your team is mid-migration and needs both formats from one source of truth.
| Bruno vs your real API | Bruno vs a hosted mock | |
|---|---|---|
| Contract-testing the actual backend | โ point it at the real thing | โ it's a mock โ wrong tool |
| Learning/teaching Bruno itself | needs a backend first | โ exists, seeded, breakable |
| Practicing auth flows, retries, error branches | risky against prod | โ forced 503s/delays/chaos on demand |
| Backend doesn't exist yet | โ | โ its whole job |
| Airgapped/offline runs | โ | โ needs egress; Bruno itself is offline-first |
Projects have a 10,000 requests/day cap โ collections and smoke suites barely dent it, but don't point a load test at us.
npm install -g @usebruno/cli
bru import openapi -s https://mockbird.mockbird.workers.dev/m/demo/openapi.json -o demo-collection
cd demo-collection && bru run --env "Environment 1"
Or paste the .bru files from ยง2 into a folder and bru run. Free, no signup required. Full docs ยท machine-readable API index.
Related: the same walkthrough for Hoppscotch, escaping Postman's mock-server limits, mock server from an OpenAPI spec, the mock JWT auth API, mock APIs in GitHub Actions, and testing loading and error states.
Verification: every command and .bru file on this page was run verbatim with Bru CLI 4.0.0 (installed fresh from npm on 31 Aug 2026) against production: the OpenAPI import produced the tree shown, the 4-request suite passed 8/8 assertions, the auth chain passed, the mock_delay latency assertion passed at ~1.6 s, and the intentional failure case exited 1. Test records created on the demo were deleted afterwards. If a snippet here doesn't work, that's a bug: tell us.