โ† All guides

A real API to try Bruno on

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.

1. A whole collection in one command

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.)

2. A hand-written .bru suite that passes verbatim

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

3. A JWT auth flow to practice on

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.

4. Latency and flakiness you control

?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.

5. CI: it's just an exit code

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"

6. Your own API instead of the shared demo

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.

Where this fits (honestly)

Bruno vs your real APIBruno vs a hosted mock
Contract-testing the actual backendโœ” point it at the real thingโœ˜ it's a mock โ€” wrong tool
Learning/teaching Bruno itselfneeds a backend firstโœ” exists, seeded, breakable
Practicing auth flows, retries, error branchesrisky 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.

Try it now

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.