Modern API development requires more than a tool that can send HTTP requests.
Developers need API collections that can be reviewed, versioned, tested, shared, and executed inside CI pipelines. However, many API clients treat collections as data stored inside their own platform.
Bruno takes a different approach.
Bruno is an open-source, local-first, and Git-native API client. Instead of hiding API collections inside a cloud workspace, Bruno stores them as readable files on your filesystem.
That simple decision makes Bruno fit naturally into a developer's existing workflow.
What Is Bruno?
Bruno is a desktop API client for developing and testing APIs.
You can use it to work with:
- REST APIs
- GraphQL APIs
- Authentication flows
- Environment variables
- Request scripts
- Response tests
- API documentation
- Automated collection runs
It covers many workflows developers already use tools like Postman or Insomnia for.
The major difference is how Bruno stores and manages API collections.
Your requests live inside regular folders and text files. You can open them in an editor, commit them to Git, review changes in pull requests, and keep them beside your application code.
Why Git-Native API Collections Matter
Imagine that your backend repository contains the following structure:
my-api/
├── app/
├── database/
├── routes/
├── tests/
└── api-collection/
├── users/
├── authentication/
├── products/
└── bruno.json
The API collection can be committed to the same repository as the application.
When a developer changes an endpoint, they can update the related Bruno request in the same branch.
A pull request can then contain:
Updated user registration validation
Updated registration API request
Added registration error tests
Updated API documentation
The backend implementation and its API examples remain synchronized.
This is much cleaner than updating code in Git while separately maintaining a collection inside an external cloud workspace.
Collections Are Readable Files
Bruno stores requests as plain-text files.
A simplified Bruno request may look similar to this:
meta {
name: Get Current User
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/user
body: none
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}
tests {
test("should return the authenticated user", function () {
expect(res.status).to.equal(200);
});
}
Because the request is text-based, it works well with:
- Git diffs
- Pull requests
- Code reviews
- Merge history
- IDE search
- Automated agents
- Repository backups
You do not need a proprietary interface just to understand what changed.
Local-First by Design
Bruno runs locally and does not require an account for its normal open-source workflow.
Your requests, environments, scripts, and collections stay on your machine or inside repositories you control.
This is useful when working with:
- Private APIs
- Internal company services
- Sensitive authentication flows
- Client projects
- Local development environments
- Restricted enterprise networks
A local-first tool also avoids making your API workflow dependent on the availability of an external cloud platform.
Creating Your First Bruno Collection
After installing Bruno, create a new collection and select a directory on your filesystem.
For example:
projects/
└── inventory-api/
└── bruno/
Create an environment containing variables such as:
vars {
baseUrl: http://localhost:8000
accessToken:
}
You can then create a login request:
POST {{baseUrl}}/api/login
With a JSON body:
{
"email": "developer@example.com",
"password": "password"
}
After receiving the response, you can save the token using a post-response script:
if (res.status === 200) {
bru.setEnvVar("accessToken", res.body.token);
}
Other authenticated requests can reuse the value:
Authorization: Bearer {{accessToken}}
This creates a repeatable authentication workflow without manually copying tokens after every login.
Testing API Responses
Bruno supports response tests, allowing a collection to work as both documentation and executable validation.
For example:
test("status should be 200", function () {
expect(res.status).to.equal(200);
});
test("response should contain a user", function () {
expect(res.body).to.have.property("user");
});
test("user should have an email", function () {
expect(res.body.user).to.have.property("email");
});
You can also validate response types:
test("products should be an array", function () {
expect(res.body.products).to.be.an("array");
});
Or check business rules:
test("product stock should not be negative", function () {
res.body.products.forEach(function (product) {
expect(product.stock).to.be.at.least(0);
});
});
These tests help catch API regressions before they reach production.
Environments and Variables
Most applications have multiple environments:
- Local
- Development
- Staging
- Production
Instead of duplicating requests, you can create environment-specific variables.
Local environment:
vars {
baseUrl: http://localhost:8000
}
Staging environment:
vars {
baseUrl: https://staging-api.example.com
}
Production environment:
vars {
baseUrl: https://api.example.com
}
Your request remains unchanged:
GET {{baseUrl}}/api/products
You only switch the active environment.
Sensitive values such as API keys and access tokens should be stored separately from committed collection files.
Bruno automatically creates recommended Git ignore rules when creating collections, but developers should still review files before committing them.
Collaboration Through Git
A Bruno collection can be managed like any other source code directory.
Initialize Git:
git init
Add the collection:
git add .
Create a commit:
git commit -m "Add authentication API collection"
Connect it to a remote repository:
git remote add origin https://github.com/username/project-api.git
git push -u origin main
Team members can clone the repository and open the same collection in Bruno.
Changes can be reviewed using normal Git commands:
git diff
git status
git log
This gives API collections the same review process as application code.
Running Collections From the CLI
Bruno provides a command-line interface called Bruno CLI.
Install it using npm:
npm install -g @usebruno/cli
Navigate to the collection directory:
cd path/to/bruno-collection
Run the entire collection:
bru run
Run it with a selected environment:
bru run --env local
Run only a specific folder:
bru run users
You can also pass environment variables directly:
bru run --env local --env-var ACCESS_TOKEN=your-token
To run requests with selected tags:
bru run --tags=smoke,sanity
To exclude unfinished or skipped requests:
bru run --exclude-tags=skip,draft
This makes Bruno useful beyond manual API exploration.
Your collection can become part of your automated testing system.
Using Bruno in CI/CD
A simple CI workflow can install Bruno CLI and run an API collection after starting the application.
A simplified GitHub Actions example could look like this:
name: API Tests
on:
push:
branches:
- main
pull_request:
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install Bruno CLI
run: npm install -g @usebruno/cli
- name: Run API tests
working-directory: ./bruno
run: bru run --env ci
For a real application, you would also start the backend service and database before executing the collection.
For example:
- name: Install backend dependencies
run: composer install --no-interaction
- name: Prepare environment
run: |
cp .env.example .env
php artisan key:generate
php artisan migrate --force
- name: Start Laravel server
run: php artisan serve --host=127.0.0.1 --port=8000 &
- name: Run Bruno tests
working-directory: ./bruno
run: bru run --env ci
Now every pull request can verify that important API endpoints still work.
Generating Test Reports
Bruno CLI supports report formats that can be used in automated pipelines.
Depending on your workflow, you can generate formats such as:
- JSON
- JUnit
- HTML
JUnit output is especially useful because many CI platforms can display test results directly.
A collection can therefore serve several purposes:
- Manual API exploration
- Developer documentation
- Regression testing
- CI validation
- Integration testing
This reduces the need to maintain separate API examples and testing assets.
Importing Existing Collections
Teams do not need to rebuild every request manually before trying Bruno.
Bruno supports importing collections from common formats and tools, including Postman collections.
A practical migration process is:
- Export the existing collection.
- Import it into Bruno.
- Verify environment variables and authentication.
- Review scripts and tests.
- Store the new collection inside the project repository.
- Commit it to Git.
- Add CLI execution to CI.
Some tool-specific scripts may require adjustment, but importing provides a useful starting point.
API Documentation Inside the Collection
Bruno supports Markdown documentation at different levels, including:
- Workspace
- Collection
- Folder
- Request
You can document an authentication folder with information such as:
# Authentication
All protected endpoints require a bearer token.
## Login
Send the user's email and password to `/api/login`.
The response contains an access token that should be included in the
`Authorization` header for future requests.
Documentation stays close to the executable requests.
When an endpoint changes, developers can update the request, tests, and explanation together.
Bruno can also generate standalone HTML documentation from collections, making it possible to publish readable API references without maintaining a completely separate source.
Bruno 4 and AI Assistance
Bruno 4.0 introduced Bring Your Own Key AI capabilities.
Instead of forcing developers to use a fixed AI provider, Bruno supports OpenAI-compatible providers and custom model configurations.
Its AI features can assist with:
- Writing request documentation
- Generating scripts
- Creating response tests
- Reviewing changes
- Working with the active request or collection
- Providing scripting autocomplete
The Bring Your Own Key approach gives developers more control over the provider, model, and credentials they use.
Bruno also includes security controls that explain what context is shared with the selected AI provider.
AI assistance is most useful here when it improves the files developers already own instead of moving their entire API workflow into another closed platform.
Where Bruno Fits Best
Bruno is particularly suitable for teams that:
- Already use Git for collaboration
- Want collections stored beside application code
- Prefer local-first development tools
- Need readable API request files
- Want API tests inside CI pipelines
- Work with private or internal APIs
- Want to avoid mandatory cloud accounts
- Need an open-source API client
- Use coding agents or IDE-based workflows
It is also a strong option for solo developers because collections can be backed up and shared through any Git repository.
Things to Consider
Bruno's workflow may feel different if your team is accustomed to browser-based collaboration.
Git-based collaboration assumes that team members understand concepts such as:
- Repositories
- Commits
- Branches
- Pull requests
- Merge conflicts
This is usually not a major issue for engineering teams, but it can require onboarding for non-technical users.
Some advanced Git interface features and team capabilities are also associated with paid Bruno editions. You should compare the current editions before standardizing it across a larger organization.
The core open-source workflow, however, remains highly useful for individual developers and technical teams.
Bruno vs Traditional Cloud-Based API Clients
The main difference is not the request editor.
Most established API clients can send requests, manage environments, and execute tests.
The important difference is ownership and workflow.
A traditional cloud-based workflow often looks like this:
Application code -> Git repository
API collection -> External platform
Documentation -> Another platform
Automated tests -> Separate test suite
A Bruno-based workflow can look like this:
Application code -> Git repository
API collection -> Same Git repository
API documentation -> Same collection
API tests -> Same collection
CI execution -> Bruno CLI
This does not eliminate every tool, but it reduces fragmentation.
A Practical Project Structure
For a Laravel API, I would organize the project like this:
inventory-platform/
├── app/
├── database/
├── routes/
├── tests/
├── bruno/
│ ├── authentication/
│ │ ├── login.bru
│ │ ├── logout.bru
│ │ └── current-user.bru
│ ├── products/
│ │ ├── list-products.bru
│ │ ├── create-product.bru
│ │ └── update-product.bru
│ ├── orders/
│ │ ├── create-order.bru
│ │ └── get-order.bru
│ ├── environments/
│ ├── collection.bru
│ └── bruno.json
├── composer.json
└── README.md
Each backend feature should include:
- Its implementation
- Automated application tests
- Bruno requests
- Bruno response tests
- Relevant API documentation
A feature is not complete until its executable API examples are updated.
Conclusion
Bruno is not interesting simply because it is another API client.
It is interesting because it treats API collections as part of the codebase.
Collections can be stored locally, reviewed through Git, executed through the command line, validated in CI, and edited with normal developer tools.
That makes the API workflow easier to own and maintain.
For developers who value local tools, open-source software, readable files, and Git-based collaboration, Bruno is worth testing on a real project.
You can explore it at usebruno.com and keep your first collection inside your next API repository.
Top comments (0)