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.
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.
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.
npm install -g @usebruno/cli
bru run --env staging --reporter-junit results.xmlMost 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.
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.
bru run --env staging --env-var token=$API_TOKENThat 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
| Tool | Strength | Weakness | Pick it when |
|---|---|---|---|
| Bruno | Collections in the repository, offline, no account | Fewer prebuilt integrations, smaller ecosystem | The team wants requests versioned with the code |
| Postman | Largest ecosystem, strong cloud collaboration | Requires an account, data in the vendor cloud | Large organisation using monitoring and mock servers |
| Insomnia | Clean interface, good GraphQL support | Past changes to the licensing model | Work centred on GraphQL |
| Hoppscotch | Runs in the browser, nothing to install | Limits when calling local services | Quick endpoint check with no install |
| curl | Always available, great for scripting | No organisation or history | A 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.
| Plan | Cost | Workspaces | Repositories in the git interface |
|---|---|---|---|
| Open Source | 0 USD | 2 | public only |
| Pro | 6 USD per user per month, billed annually | unlimited | public and private |
| Ultimate | 11 USD per user per month, billed annually | unlimited | public 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.