We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide12 min read

Bruno, an API client that lives in your repository

Bruno keeps API collections as files in your repository, works offline, and needs no account. The bru format, tests, CLI in CI, and migrating from Postman.

Bruno, an API client that lives in your repository

Bruno stores request collections as plain text files inside the project directory. You version them with git alongside the code, review changes in a pull request, and need no account or sync with somebody else's cloud. The application works offline and its core is free and open source.

Where the idea came from

Classic API clients keep collections in their own cloud, tied to a user account. Three recurring problems follow: the collection drifts from the actual API because nobody remembers to update it, a change made by one person stays invisible to everyone else until a sync, and the security team asks where exactly internal service URLs and tokens end up.

Bruno inverts that arrangement. A collection is a directory of .bru files sitting next to the code it describes. Updating an endpoint and updating a request land in the same commit, so drift becomes visible during code review.

The side effect often matters more than the feature itself: since the collection lives in the repository, a new team member receives it along with the project. There is no step of inviting somebody to a workspace or hunting for whoever holds the latest version.

The bru format in practice

A request file reads well for both humans and diff tools.

Code
TEXT
meta {
  name: Create order
  type: http
  seq: 3
}

post {
  url: {{baseUrl}}/api/orders
  body: json
}

headers {
  Authorization: Bearer {{token}}
}

body:json {
  {
    "productId": "{{productId}}",
    "quantity": 2
  }
}

The difference against JSON exported by other tools is practical. A diff shows that a header changed, rather than that two hundred lines moved because key order differs. A merge conflict can be resolved by hand, because the file reads like configuration rather than a structure dump.

Environment variables live separately, in environment files. Sensitive values stay outside the repository and get injected through system variables or from a file listed in .gitignore.

Tests attached to requests

Each request carries JavaScript assertions that run once a response arrives.

Code
JavaScript
test("returns 201 and an order id", function() {
  expect(res.getStatus()).to.equal(201)
  expect(res.getBody().id).to.be.a("string")
})

test("responds in under 500 ms", function() {
  expect(res.getResponseTime()).to.be.below(500)
})

Pre request and post response scripts let you build a sequence: log in, save the token into a variable, use it in later steps. That covers contract testing, meaning checking whether the API still answers with what it promised.

Write assertions against what the contract promises rather than the exact shape of a response. Checking that a field exists and carries the right type survives the backend team adding a new field. Comparing a whole response against a stored fixture breaks on the first such change and teaches the team to ignore red tests.

It is not a substitute for tests in code, though. Test business logic inside the project and treat Bruno as the layer checking the seam between services, and as documentation you can actually execute.

Running it in a pipeline

The command line client executes a whole collection without the interface, so the same requests verify an environment after deployment.

Code
Bash
npm install -g @usebruno/cli
bru run --env staging --reporter-junit results.xml

Most continuous integration systems read JUnit reports, so a failing contract test blocks a release exactly as a failing unit test does. That is the most commonly overlooked value of this tool: the collection stops being one person's private toy and becomes part of the pipeline.

A practical arrangement: after deploying to staging you run the collection with assertions on the critical paths. At twenty requests a run takes a dozen seconds and catches configuration mistakes unit tests never see, because they never touch the network.

Authentication and secrets

Most APIs want a token, and obtaining one is usually a separate request. Instead of pasting the value by hand each session, save it in a post response script and use it as a variable.

Code
JavaScript
const data = res.getBody()
bru.setEnvVar("token", data.access_token)

From then on, later requests reference {{token}}, and refreshing comes down to running one call again. For APIs with short lived tokens, put that step first in execution order so a pipeline run begins with authentication.

Do not keep secrets in environment files that reach the repository. A safe arrangement has three layers: an environment file with URLs and variable names in the repository, sensitive values in a local file excluded from git, and in the pipeline variables injected from the CI secret store.

Code
Bash
bru run --env staging --env-var token=$API_TOKEN

That split carries another benefit. A new team member receives the full set of requests with the repository and can see which values are missing, because variable names are visible. Rather than guessing, they ask for two specific secrets.

If tokens previously reached git history, removing them from the file is not enough. They have to be revoked with the provider, since history keeps them available to anyone who clones the repository.

Organising collections in a larger project

A collection of thirty requests in one directory stops being legible after a month. The structure that holds up over time mirrors how services are divided, not the order in which requests were created.

A sensible layout is a directory per resource, with requests inside ordered by life cycle: create, read, update, delete. The seq field in metadata sets execution order, which matters when running a whole collection in a pipeline where later steps depend on earlier ones.

In a monorepo it pays to keep the collection next to the service it describes rather than in one shared top level directory. An API change and a request change then land in the same area of a pull request and reach the same reviewer.

Naming matters more than it seems, since the file name is what shows in a diff. A name describing the action, creating an order with a discount for instance, tells a reviewer more than the endpoint path, which sits in the file body anyway.

The last thing is pruning. Collections tend to accumulate requests created ad hoc while diagnosing problems. Once a quarter it is worth reviewing the directory and deleting those that describe no real path, otherwise after a year half the contents are experiments nobody understands.

Bruno against the alternatives

ToolStrengthWeaknessPick it when
BrunoCollections in the repository, offline, no accountFewer prebuilt integrations, smaller ecosystemThe team wants requests versioned with the code
PostmanLargest ecosystem, strong cloud collaborationRequires an account, data in the vendor cloudLarge organisation using monitoring and mock servers
InsomniaClean interface, good GraphQL supportPast changes to the licensing modelWork centred on GraphQL
HoppscotchRuns in the browser, nothing to installLimits when calling local servicesQuick endpoint check with no install
curlAlways available, great for scriptingNo organisation or historyA single call in the terminal

The choice mostly depends on whether the collection should be a team artefact or a service. If the team works in git and avoids yet another account, Bruno fits that habit. If you already use API monitoring and mock servers from one vendor, switching buys little.

Licence and payment model

The application core is free and open source, and that is not changing. Around it, the vendor sells support and features aimed at organisations.

One change is worth knowing, since plenty of outdated information circulates. The Golden Edition, bought once for 19 USD, was retired for new customers. The reason was that licences intended for individuals were mostly purchased by companies, which did not match the intent. Anyone who bought it earlier keeps access for good. The current offer is the open source build plus two paid plans, both billed annually.

PlanCostWorkspacesRepositories in the git interface
Open Source0 USD2public only
Pro6 USD per user per month, billed annuallyunlimitedpublic and private
Ultimate11 USD per user per month, billed annuallyunlimitedpublic and private

The 6 and 11 USD figures are the rates when paying a year up front, since the vendor lists no monthly option. When budgeting, multiply by twelve rather than assuming a fee you can drop in any given month. Ultimate comes with a fourteen day trial that needs no card.

The free version is not boundless either. It holds two workspaces and five OpenAPI syncs per month, and its built in git interface connects to public repositories only. Support means the community, with no guaranteed response time. Keeping collections in a repository and driving plain git from a terminal stay free regardless, because .bru files sit on disk and need no integration at all.

That detail matters when adding the tool to a company stack. Check current pricing before assuming a one off fee, because 2024 material still describes it.

Migrating from Postman

Collection import works and is a starting point, not the end of the job. Requests, headers, environments, and most scripts transfer, while anything specific to the previous tool needs manual attention.

Three notes from practice. Scripts using the Postman specific object have to be rewritten to Bruno equivalents, which takes about an hour for a dozen requests. Global variables scattered across a workspace are worth tidying into environment files while you are at it. Secrets once pasted straight into headers must be caught before the collection reaches the repository, since from that point they live in git history.

A sensible order is migrating one collection, working with it for a week, and only then moving the rest. Importing everything at once produces a directory of a hundred requests, half of which fail, and a discouraged team.

The collection as API documentation

API documentation ages faster than any other document in a project, because nobody notices when it stops being true. A collection running automatically has the advantage of admitting it: the run starts failing.

For that to work, requests have to describe real use cases rather than only the happy path. Alongside the call returning 201, keep a variant with a missing field and assert that the API answers 400 with a sensible message. That second case breaks more often, precisely because it is tested less, and it is the one that reaches a customer integrating with your API.

Writing a short note in the docs field of a request is good practice. A few sentences about when this call is used and what to expect from it costs a minute and saves questions in team chat. Unlike an external document, that note sits next to the request, so the person changing the behaviour updates it.

It also helps to separate two things that get conflated. An OpenAPI specification describes the shape of an API formally and suits client generation. A Bruno collection shows how the API is actually used, including call order and real values. The two layers complement rather than replace each other.

For a public API, a collection is often the fastest way for an outsider to get started. A directory of requests in a repository beats a page of examples, because it runs without retyping anything.

Common mistakes

The first is keeping tokens inside collection files. Anything that enters the repository stays in history, so inject sensitive values through environment variables.

The second is treating the collection as documentation without running it in a pipeline. A collection nobody executes automatically goes stale at the same rate as a document in a word processor.

The third is duplicating requests instead of using variables. Ten variants of the same call with different URLs become a problem the first time a path changes.

The fourth is documenting only the paths that work. A set of correct calls looks tidy and guards nothing, because API bugs mostly live in edge cases rather than in what you check by hand on every release anyway.

The fifth is skipping environments. Working in one environment with a manually swapped URL sooner or later sends a request to production instead of staging.

FAQ

Is Bruno free?

The application core is free and open source and covers daily API work. The paid Pro and Ultimate plans, at 6 and 11 USD per user per month billed annually, add support and team features. The one off Golden Edition licence was retired for new customers, while those who bought it keep access.

Can I move collections from Postman?

Yes, import handles collections and environments. Manual work remains for scripts using Postman specific objects and for tidying variables. Migrating collection by collection beats moving everything at once.

Does Bruno support GraphQL?

Yes, alongside REST it handles GraphQL with a separate field for variables. If GraphQL is central to the project, it is worth comparing the experience against Insomnia, which has extensive support in that area.

Does it work without internet access?

Yes, that is one of the project's premises. The application requires no account or sign in, and data stays on disk. A network connection is needed only for sending the requests themselves.

Is it suitable for continuous integration?

Yes, through the command line client that runs a collection and returns a JUnit report. That covers contract testing after deployment, while load testing calls for a tool built for the purpose.

Documentation sits at docs.usebruno.com, and the source code in the GitHub repository.