Bruno - Open-Source API Client
Czym jest Bruno?
Bruno to nowoczesny, open-source klient API zaprojektowany jako alternatywa dla Postmana z fundamentalną różnicą w podejściu: kolekcje API są zapisywane jako pliki tekstowe w formacie .bru, które możesz commitować bezpośrednio do repozytorium Git. To sprawia, że Bruno jest idealnym narzędziem dla zespołów, które chcą wersjonować swoje kolekcje API razem z kodem źródłowym.
Bruno działa całkowicie offline, bez konieczności zakładania konta czy korzystania z chmury. Twoje dane pozostają na Twoim komputerze, co jest kluczowe dla firm dbających o bezpieczeństwo i prywatność danych.
Dlaczego Bruno?
Kluczowe zalety
- Git-friendly - Kolekcje jako pliki tekstowe w repozytorium
- Offline-first - Działa bez konta i internetu
- Open-source - MIT License, aktywny development
- Prywatność - Dane zostają lokalnie na Twoim komputerze
- Darmowy - Żadnych płatnych planów, wszystko za darmo
- Cross-platform - Windows, macOS, Linux
- Scripting - JavaScript do pre-request i testĂłw
- Environments - Zmienne środowiskowe w plikach
Bruno vs Postman vs Insomnia
| Cecha | Bruno | Postman | Insomnia |
|---|---|---|---|
| Cena | Darmowy | Freemium ($14/mo) | Freemium ($5/mo) |
| Storage | Lokalne pliki | Cloud | Cloud/Local |
| Git integration | Natywnie | Export/Import | Plugin |
| Offline | Pełny | Ograniczony | Tak |
| Open-source | Tak (MIT) | Nie | Częściowo |
| Scripting | JavaScript | JavaScript | JavaScript |
| Team collaboration | Via Git | Wbudowane | Wbudowane |
| GraphQL | Tak | Tak | Tak |
| gRPC | Tak | Tak | Tak |
| WebSocket | Tak | Tak | Tak |
| Konto wymagane | Nie | Tak | Opcjonalne |
Kiedy wybrać Bruno?
Idealne dla:
- Zespołów używających Git
- Firm dbających o prywatność
- ProjektĂłw open-source
- Ĺšrodowisk bez internetu
- DeveloperĂłw ceniÄ…cych prostotÄ™
RozwaĹĽ alternatywy gdy:
- Potrzebujesz wbudowanego team collaboration bez Git
- Wymagasz zaawansowanych integracji enterprise
- Preferujesz cloud-based workflow
Instalacja
Desktop App
# macOS (Homebrew)
brew install bruno
# Windows (Chocolatey)
choco install bruno
# Windows (Scoop)
scoop install bruno
# Linux (Snap)
sudo snap install bruno
# Linux (AppImage)
# Pobierz z https://www.usebruno.com/downloadsCLI (Bruno CLI)
# npm
npm install -g @usebruno/cli
# Weryfikacja
bru --versionStruktura projektu
Organizacja kolekcji
Bruno organizuje kolekcje w katalogach z plikami .bru:
api-collection/
├── bruno.json # Konfiguracja kolekcji
├── environments/
│ ├── local.bru # Środowisko lokalne
│ ├── staging.bru # Środowisko staging
│ └── production.bru # Środowisko produkcyjne
├── users/
│ ├── folder.bru # Metadata folderu
│ ├── get-all-users.bru
│ ├── get-user-by-id.bru
│ ├── create-user.bru
│ ├── update-user.bru
│ └── delete-user.bru
├── posts/
│ ├── folder.bru
│ ├── list-posts.bru
│ └── create-post.bru
└── auth/
├── folder.bru
├── login.bru
└── refresh-token.bruPlik bruno.json
{
"version": "1",
"name": "My API Collection",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}Format .bru
Podstawowa struktura ĹĽÄ…dania
meta {
name: Get All Users
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/users
body: none
auth: none
}
headers {
Content-Type: application/json
Accept: application/json
}
query {
page: 1
limit: 20
sort: -created
}POST z body JSON
meta {
name: Create User
type: http
seq: 2
}
post {
url: {{baseUrl}}/api/users
body: json
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}
headers {
Content-Type: application/json
}
body:json {
{
"name": "Jan Kowalski",
"email": "jan@example.com",
"role": "user",
"settings": {
"notifications": true,
"theme": "dark"
}
}
}Form Data
meta {
name: Upload Avatar
type: http
seq: 3
}
post {
url: {{baseUrl}}/api/users/{{userId}}/avatar
body: multipartForm
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}
body:multipart-form {
avatar: @file(/path/to/avatar.png)
description: Profile photo
}GraphQL
meta {
name: Get User with Posts
type: graphql
seq: 1
}
post {
url: {{baseUrl}}/graphql
body: graphql
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}
body:graphql {
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
createdAt
}
}
}
}
body:graphql:vars {
{
"id": "{{userId}}"
}
}Environments (Ĺšrodowiska)
Definicja środowiska
# environments/local.bru
vars {
baseUrl: http://localhost:3000
apiVersion: v1
}
vars:secret [
accessToken,
refreshToken,
apiKey
]# environments/staging.bru
vars {
baseUrl: https://staging-api.example.com
apiVersion: v1
}
vars:secret [
accessToken,
refreshToken,
apiKey
]# environments/production.bru
vars {
baseUrl: https://api.example.com
apiVersion: v1
}
vars:secret [
accessToken,
refreshToken,
apiKey
]UĹĽywanie zmiennych
get {
url: {{baseUrl}}/api/{{apiVersion}}/users/{{userId}}
body: none
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}Zmienne dynamiczne
// W script:pre-request
bru.setVar("timestamp", Date.now());
bru.setVar("randomId", Math.random().toString(36).substring(7));
bru.setVar("isoDate", new Date().toISOString());Scripting
Pre-request scripts
meta {
name: Create Order
type: http
seq: 1
}
post {
url: {{baseUrl}}/api/orders
body: json
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}
body:json {
{
"orderId": "{{orderId}}",
"timestamp": "{{timestamp}}",
"items": [
{"productId": "123", "quantity": 2}
]
}
}
script:pre-request {
// Generuj unikalne ID zamĂłwienia
const orderId = `ORD-${Date.now()}-${Math.random().toString(36).substring(7)}`;
bru.setVar("orderId", orderId);
// Timestamp
bru.setVar("timestamp", new Date().toISOString());
// Pobierz dane z poprzedniego response (jeśli zapisane)
const savedToken = bru.getVar("accessToken");
if (!savedToken) {
console.log("Warning: No access token found");
}
}Post-response scripts
script:post-response {
// Zapisz token z response
if (res.body && res.body.accessToken) {
bru.setVar("accessToken", res.body.accessToken);
bru.setVar("refreshToken", res.body.refreshToken);
}
// Zapisz ID do użycia w następnych requestach
if (res.body && res.body.id) {
bru.setVar("lastCreatedId", res.body.id);
}
// Log dla debugging
console.log("Response status:", res.status);
console.log("Response time:", res.responseTime, "ms");
}Dostępne API w skryptach
// Zmienne
bru.setVar("name", "value"); // Ustaw zmiennÄ…
const value = bru.getVar("name"); // Pobierz zmiennÄ…
bru.setEnvVar("name", "value"); // Ustaw env var
const env = bru.getEnvVar("name"); // Pobierz env var
// Request info
const url = req.getUrl();
const method = req.getMethod();
const headers = req.getHeaders();
const body = req.getBody();
// Response info (tylko w post-response)
const status = res.status;
const statusText = res.statusText;
const headers = res.headers;
const body = res.body;
const responseTime = res.responseTime;
// Utilities
const crypto = require('crypto');
const uuid = require('uuid');
// HMAC signature example
const signature = crypto
.createHmac('sha256', bru.getVar('apiSecret'))
.update(body)
.digest('hex');
bru.setVar("signature", signature);Testy
Podstawowe asercje
meta {
name: Get User
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/users/{{userId}}
body: none
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}
tests {
// Status code
test("should return 200 OK", function() {
expect(res.status).to.equal(200);
});
// Response body
test("should return user object", function() {
expect(res.body).to.have.property('id');
expect(res.body).to.have.property('email');
expect(res.body).to.have.property('name');
});
// Type checking
test("user id should be string", function() {
expect(res.body.id).to.be.a('string');
});
// Value validation
test("email should be valid format", function() {
expect(res.body.email).to.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
});
}Zaawansowane testy
tests {
// Array validation
test("should return array of users", function() {
expect(res.body.users).to.be.an('array');
expect(res.body.users.length).to.be.greaterThan(0);
});
// Nested properties
test("users should have required fields", function() {
res.body.users.forEach(user => {
expect(user).to.have.property('id');
expect(user).to.have.property('email');
expect(user).to.have.property('createdAt');
});
});
// Response time
test("response should be fast", function() {
expect(res.responseTime).to.be.below(500);
});
// Headers
test("should have correct content type", function() {
expect(res.headers['content-type']).to.include('application/json');
});
// Conditional tests
test("pagination should work", function() {
if (res.body.meta) {
expect(res.body.meta).to.have.property('page');
expect(res.body.meta).to.have.property('totalPages');
expect(res.body.meta.page).to.equal(1);
}
});
// Save data for next requests
test("save first user id", function() {
if (res.body.users && res.body.users[0]) {
bru.setVar("testUserId", res.body.users[0].id);
}
});
}Schema validation
tests {
test("response should match schema", function() {
const schema = {
type: 'object',
required: ['id', 'email', 'name', 'createdAt'],
properties: {
id: { type: 'string' },
email: { type: 'string', format: 'email' },
name: { type: 'string', minLength: 1 },
createdAt: { type: 'string', format: 'date-time' },
role: { type: 'string', enum: ['user', 'admin', 'moderator'] }
}
};
// Bruno uses chai for assertions
// For JSON schema validation, you might use additional libraries
expect(res.body).to.have.all.keys('id', 'email', 'name', 'createdAt');
});
}Bruno CLI
Uruchamianie kolekcji
# Uruchom całą kolekcję
bru run
# Uruchom konkretny folder
bru run users/
# Uruchom pojedynczy request
bru run users/get-all-users.bru
# Z konkretnym środowiskiem
bru run --env staging
# Z outputem JSON
bru run --output results.json
# Recursive (wszystkie subfolders)
bru run --recursiveOpcje CLI
# Insecure mode (ignore SSL errors)
bru run --insecure
# Bail on first failure
bru run --bail
# Parallel execution
bru run --parallel
# Custom timeout (ms)
bru run --timeout 30000
# Verbose output
bru run --verbose
# Filter by tag
bru run --tag smoke-testsCI/CD Integration
# .github/workflows/api-tests.yml
name: API Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Bruno CLI
run: npm install -g @usebruno/cli
- name: Run API Tests
run: |
cd api-collection
bru run --env staging --output results.json
env:
BRUNO_API_KEY: ${{ secrets.API_KEY }}
BRUNO_ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
- name: Upload Results
uses: actions/upload-artifact@v4
if: always()
with:
name: api-test-results
path: api-collection/results.jsonEnvironment variables w CI
# Eksportuj zmienne przed uruchomieniem
export BRUNO_accessToken="your-token-here"
export BRUNO_apiKey="your-api-key"
# Bruno automatycznie uĹĽywa zmiennych z prefixem BRUNO_
bru run --env productionAutentykacja
Bearer Token
meta {
name: Protected Request
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/protected
body: none
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}Basic Auth
meta {
name: Basic Auth Request
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/data
body: none
auth: basic
}
auth:basic {
username: {{username}}
password: {{password}}
}API Key
meta {
name: API Key Request
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/data
body: none
auth: none
}
headers {
X-API-Key: {{apiKey}}
}OAuth2 Flow
# 1. Login request
meta {
name: Login
type: http
seq: 1
}
post {
url: {{baseUrl}}/auth/login
body: json
auth: none
}
body:json {
{
"email": "{{email}}",
"password": "{{password}}"
}
}
script:post-response {
if (res.status === 200 && res.body.accessToken) {
bru.setVar("accessToken", res.body.accessToken);
bru.setVar("refreshToken", res.body.refreshToken);
bru.setVar("tokenExpiry", res.body.expiresIn);
}
}
tests {
test("login successful", function() {
expect(res.status).to.equal(200);
expect(res.body).to.have.property('accessToken');
});
}# 2. Refresh Token
meta {
name: Refresh Token
type: http
seq: 2
}
post {
url: {{baseUrl}}/auth/refresh
body: json
auth: none
}
body:json {
{
"refreshToken": "{{refreshToken}}"
}
}
script:post-response {
if (res.status === 200) {
bru.setVar("accessToken", res.body.accessToken);
}
}AWS Signature v4
meta {
name: AWS S3 Request
type: http
seq: 1
}
get {
url: {{awsUrl}}/bucket/object
body: none
auth: awsv4
}
auth:awsv4 {
accessKeyId: {{awsAccessKeyId}}
secretAccessKey: {{awsSecretAccessKey}}
region: {{awsRegion}}
service: s3
}Zaawansowane uĹĽycie
Collection-level scripts
// collection.bru - skrypt uruchamiany dla kaĹĽdego requestu
script:pre-request {
// Dodaj timestamp do kaĹĽdego requestu
const timestamp = Date.now().toString();
req.setHeader('X-Request-Timestamp', timestamp);
// Log request info
console.log(`[${req.getMethod()}] ${req.getUrl()}`);
}
script:post-response {
// Log response info
console.log(`Response: ${res.status} in ${res.responseTime}ms`);
// Save to collection variables
if (res.headers['x-request-id']) {
bru.setVar('lastRequestId', res.headers['x-request-id']);
}
}Chaining requests
# 1. Create user
meta {
name: Create User
type: http
seq: 1
}
post {
url: {{baseUrl}}/api/users
body: json
auth: bearer
}
body:json {
{
"name": "Test User",
"email": "test@example.com"
}
}
script:post-response {
bru.setVar("createdUserId", res.body.id);
}# 2. Get created user (uses variable from previous request)
meta {
name: Get Created User
type: http
seq: 2
}
get {
url: {{baseUrl}}/api/users/{{createdUserId}}
body: none
auth: bearer
}
tests {
test("user exists", function() {
expect(res.status).to.equal(200);
expect(res.body.id).to.equal(bru.getVar("createdUserId"));
});
}Tagging i organizacja
meta {
name: Critical Health Check
type: http
seq: 1
}
docs {
This is a critical endpoint that should always be tested.
Used for monitoring and alerting.
}
get {
url: {{baseUrl}}/health
body: none
auth: none
}
assert {
res.status: eq 200
res.responseTime: lt 1000
}Assertions (alternatywa dla tests)
assert {
res.status: eq 200
res.body.success: eq true
res.body.users: isArray
res.body.users.length: gt 0
res.responseTime: lt 500
}Best practices
Struktura kolekcji
api-collection/
├── bruno.json
├── README.md
├── environments/
│ ├── local.bru
│ ├── development.bru
│ ├── staging.bru
│ └── production.bru
├── auth/
│ ├── folder.bru
│ ├── login.bru
│ ├── logout.bru
│ ├── refresh.bru
│ └── register.bru
├── users/
│ ├── folder.bru
│ ├── crud/
│ │ ├── create.bru
│ │ ├── read.bru
│ │ ├── update.bru
│ │ └── delete.bru
│ └── admin/
│ └── list-all.bru
└── _shared/
├── health-check.bru
└── version.bruKonwencje nazewnictwa
# Requests
[HTTP_METHOD]-[resource]-[action].bru
# Przykłady
get-users.bru
get-user-by-id.bru
post-create-user.bru
put-update-user.bru
delete-user.bru
# Foldery
lowercase-with-dashes/Secrets management
# .gitignore
environments/*.secret.bru
.env
.env.local
# Trzymaj sekrety poza repozytorium
# Używaj zmiennych środowiskowych w CI/CDDokumentacja w kolekcji
meta {
name: Create Order
type: http
seq: 1
}
docs {
## Create a new order
Creates a new order in the system.
### Required fields:
- `items`: Array of items with productId and quantity
- `shippingAddress`: Delivery address object
### Response:
- `201 Created`: Order created successfully
- `400 Bad Request`: Invalid input data
- `401 Unauthorized`: Missing or invalid token
}
post {
url: {{baseUrl}}/api/orders
body: json
auth: bearer
}Integracje
VS Code Extension
Bruno oferuje rozszerzenie VS Code do podglÄ…du i edycji plikĂłw .bru:
# Zainstaluj z VS Code Marketplace
# Szukaj: "Bruno"Import z Postman
# W Bruno App:
# File > Import Collection > Postman Collection
# Obsługiwane formaty:
# - Postman Collection v2.1
# - Postman Environment
# - OpenAPI/Swagger
# - InsomniaExport do OpenAPI
Bruno może eksportować kolekcje do formatu OpenAPI/Swagger dla dokumentacji.
FAQ - Najczęściej zadawane pytania
Jak migrować z Postmana?
- W Postmanie: Export Collection (Collection v2.1)
- W Bruno: File > Import Collection
- Wybierz plik .json
- Sprawdź i dostosuj zmienne środowiskowe
Czy Bruno obsługuje team collaboration?
Tak, poprzez Git. Cały zespół może pracować na tej samej kolekcji:
- KaĹĽdy ma lokalnÄ… kopiÄ™
- Zmiany sÄ… mergowane przez Git
- Code review dla zmian w API
Jak używać Bruno z CI/CD?
# Zainstaluj CLI w pipeline
npm install -g @usebruno/cli
# Uruchom testy
bru run --env staging
# Z outputem dla raportĂłw
bru run --output results.jsonCzy mogę używać bibliotek npm w skryptach?
Bruno ma wbudowane niektĂłre biblioteki:
crypto- kryptografiauuid- generowanie UUID- Standardowe JavaScript API
Dla bardziej zaawansowanych potrzeb rozważ pre-processing zewnętrzne.
Jak debugować skrypty?
// UĹĽywaj console.log
script:pre-request {
console.log("Request URL:", req.getUrl());
console.log("Headers:", JSON.stringify(req.getHeaders(), null, 2));
console.log("Variables:", {
baseUrl: bru.getVar("baseUrl"),
token: bru.getVar("accessToken")
});
}Podsumowanie
Bruno to doskonała alternatywa dla Postmana dla zespołów, które:
- Używają Git - Kolekcje jako kod źródłowy
- Cenią prywatność - Dane pozostają lokalne
- Potrzebują offline - Pełna funkcjonalność bez internetu
- Preferują open-source - MIT License, aktywna społeczność
- Chcą oszczędzić - Wszystko za darmo
Bruno sprawia, że API testing staje się częścią normalnego workflow developmentu, z code review, wersjonowaniem i CI/CD integration.
Bruno - open-source API client
What is Bruno?
Bruno is a modern, open-source API client designed as a Postman alternative with a fundamental difference in approach: API collections are stored as text files in .bru format that you can commit directly to your Git repository. This makes Bruno the ideal tool for teams that want to version their API collections alongside their source code.
Bruno works entirely offline, without any need to create an account or use the cloud. Your data stays on your computer, which is crucial for companies that care about data security and privacy.
Why Bruno?
Key advantages
- Git-friendly - Collections as text files in your repository
- Offline-first - Works without an account or internet connection
- Open-source - MIT License, active development
- Privacy - Data stays locally on your computer
- Free - No paid plans, everything is free
- Cross-platform - Windows, macOS, Linux
- Scripting - JavaScript for pre-request scripts and tests
- Environments - Environment variables stored in files
Bruno vs Postman vs Insomnia
| Feature | Bruno | Postman | Insomnia |
|---|---|---|---|
| Price | Free | Freemium ($14/mo) | Freemium ($5/mo) |
| Storage | Local files | Cloud | Cloud/Local |
| Git integration | Native | Export/Import | Plugin |
| Offline | Full | Limited | Yes |
| Open-source | Yes (MIT) | No | Partially |
| Scripting | JavaScript | JavaScript | JavaScript |
| Team collaboration | Via Git | Built-in | Built-in |
| GraphQL | Yes | Yes | Yes |
| gRPC | Yes | Yes | Yes |
| WebSocket | Yes | Yes | Yes |
| Account required | No | Yes | Optional |
When to choose Bruno?
Ideal for:
- Teams using Git
- Companies that care about privacy
- Open-source projects
- Environments without internet access
- Developers who value simplicity
Consider alternatives when:
- You need built-in team collaboration without Git
- You require advanced enterprise integrations
- You prefer a cloud-based workflow
Installation
Desktop App
# macOS (Homebrew)
brew install bruno
# Windows (Chocolatey)
choco install bruno
# Windows (Scoop)
scoop install bruno
# Linux (Snap)
sudo snap install bruno
# Linux (AppImage)
# Download from https://www.usebruno.com/downloadsCLI (Bruno CLI)
# npm
npm install -g @usebruno/cli
# Verification
bru --versionProject structure
Collection organization
Bruno organizes collections in directories with .bru files:
api-collection/
├── bruno.json # Collection configuration
├── environments/
│ ├── local.bru # Local environment
│ ├── staging.bru # Staging environment
│ └── production.bru # Production environment
├── users/
│ ├── folder.bru # Folder metadata
│ ├── get-all-users.bru
│ ├── get-user-by-id.bru
│ ├── create-user.bru
│ ├── update-user.bru
│ └── delete-user.bru
├── posts/
│ ├── folder.bru
│ ├── list-posts.bru
│ └── create-post.bru
└── auth/
├── folder.bru
├── login.bru
└── refresh-token.bruThe bruno.json file
{
"version": "1",
"name": "My API Collection",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}The .bru format
Basic request structure
meta {
name: Get All Users
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/users
body: none
auth: none
}
headers {
Content-Type: application/json
Accept: application/json
}
query {
page: 1
limit: 20
sort: -created
}POST with JSON body
meta {
name: Create User
type: http
seq: 2
}
post {
url: {{baseUrl}}/api/users
body: json
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}
headers {
Content-Type: application/json
}
body:json {
{
"name": "Jan Kowalski",
"email": "jan@example.com",
"role": "user",
"settings": {
"notifications": true,
"theme": "dark"
}
}
}Form Data
meta {
name: Upload Avatar
type: http
seq: 3
}
post {
url: {{baseUrl}}/api/users/{{userId}}/avatar
body: multipartForm
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}
body:multipart-form {
avatar: @file(/path/to/avatar.png)
description: Profile photo
}GraphQL
meta {
name: Get User with Posts
type: graphql
seq: 1
}
post {
url: {{baseUrl}}/graphql
body: graphql
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}
body:graphql {
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
createdAt
}
}
}
}
body:graphql:vars {
{
"id": "{{userId}}"
}
}Environments
Environment definition
# environments/local.bru
vars {
baseUrl: http://localhost:3000
apiVersion: v1
}
vars:secret [
accessToken,
refreshToken,
apiKey
]# environments/staging.bru
vars {
baseUrl: https://staging-api.example.com
apiVersion: v1
}
vars:secret [
accessToken,
refreshToken,
apiKey
]# environments/production.bru
vars {
baseUrl: https://api.example.com
apiVersion: v1
}
vars:secret [
accessToken,
refreshToken,
apiKey
]Using variables
get {
url: {{baseUrl}}/api/{{apiVersion}}/users/{{userId}}
body: none
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}Dynamic variables
// In script:pre-request
bru.setVar("timestamp", Date.now());
bru.setVar("randomId", Math.random().toString(36).substring(7));
bru.setVar("isoDate", new Date().toISOString());Scripting
Pre-request scripts
meta {
name: Create Order
type: http
seq: 1
}
post {
url: {{baseUrl}}/api/orders
body: json
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}
body:json {
{
"orderId": "{{orderId}}",
"timestamp": "{{timestamp}}",
"items": [
{"productId": "123", "quantity": 2}
]
}
}
script:pre-request {
// Generate unique order ID
const orderId = `ORD-${Date.now()}-${Math.random().toString(36).substring(7)}`;
bru.setVar("orderId", orderId);
// Timestamp
bru.setVar("timestamp", new Date().toISOString());
// Retrieve data from previous response (if saved)
const savedToken = bru.getVar("accessToken");
if (!savedToken) {
console.log("Warning: No access token found");
}
}Post-response scripts
script:post-response {
// Save token from response
if (res.body && res.body.accessToken) {
bru.setVar("accessToken", res.body.accessToken);
bru.setVar("refreshToken", res.body.refreshToken);
}
// Save ID for use in subsequent requests
if (res.body && res.body.id) {
bru.setVar("lastCreatedId", res.body.id);
}
// Log for debugging
console.log("Response status:", res.status);
console.log("Response time:", res.responseTime, "ms");
}Available API in scripts
// Variables
bru.setVar("name", "value"); // Set a variable
const value = bru.getVar("name"); // Get a variable
bru.setEnvVar("name", "value"); // Set an env var
const env = bru.getEnvVar("name"); // Get an env var
// Request info
const url = req.getUrl();
const method = req.getMethod();
const headers = req.getHeaders();
const body = req.getBody();
// Response info (only in post-response)
const status = res.status;
const statusText = res.statusText;
const headers = res.headers;
const body = res.body;
const responseTime = res.responseTime;
// Utilities
const crypto = require('crypto');
const uuid = require('uuid');
// HMAC signature example
const signature = crypto
.createHmac('sha256', bru.getVar('apiSecret'))
.update(body)
.digest('hex');
bru.setVar("signature", signature);Tests
Basic assertions
meta {
name: Get User
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/users/{{userId}}
body: none
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}
tests {
// Status code
test("should return 200 OK", function() {
expect(res.status).to.equal(200);
});
// Response body
test("should return user object", function() {
expect(res.body).to.have.property('id');
expect(res.body).to.have.property('email');
expect(res.body).to.have.property('name');
});
// Type checking
test("user id should be string", function() {
expect(res.body.id).to.be.a('string');
});
// Value validation
test("email should be valid format", function() {
expect(res.body.email).to.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
});
}Advanced tests
tests {
// Array validation
test("should return array of users", function() {
expect(res.body.users).to.be.an('array');
expect(res.body.users.length).to.be.greaterThan(0);
});
// Nested properties
test("users should have required fields", function() {
res.body.users.forEach(user => {
expect(user).to.have.property('id');
expect(user).to.have.property('email');
expect(user).to.have.property('createdAt');
});
});
// Response time
test("response should be fast", function() {
expect(res.responseTime).to.be.below(500);
});
// Headers
test("should have correct content type", function() {
expect(res.headers['content-type']).to.include('application/json');
});
// Conditional tests
test("pagination should work", function() {
if (res.body.meta) {
expect(res.body.meta).to.have.property('page');
expect(res.body.meta).to.have.property('totalPages');
expect(res.body.meta.page).to.equal(1);
}
});
// Save data for next requests
test("save first user id", function() {
if (res.body.users && res.body.users[0]) {
bru.setVar("testUserId", res.body.users[0].id);
}
});
}Schema validation
tests {
test("response should match schema", function() {
const schema = {
type: 'object',
required: ['id', 'email', 'name', 'createdAt'],
properties: {
id: { type: 'string' },
email: { type: 'string', format: 'email' },
name: { type: 'string', minLength: 1 },
createdAt: { type: 'string', format: 'date-time' },
role: { type: 'string', enum: ['user', 'admin', 'moderator'] }
}
};
// Bruno uses chai for assertions
// For JSON schema validation, you might use additional libraries
expect(res.body).to.have.all.keys('id', 'email', 'name', 'createdAt');
});
}Bruno CLI
Running collections
# Run the entire collection
bru run
# Run a specific folder
bru run users/
# Run a single request
bru run users/get-all-users.bru
# With a specific environment
bru run --env staging
# With JSON output
bru run --output results.json
# Recursive (all subfolders)
bru run --recursiveCLI options
# Insecure mode (ignore SSL errors)
bru run --insecure
# Bail on first failure
bru run --bail
# Parallel execution
bru run --parallel
# Custom timeout (ms)
bru run --timeout 30000
# Verbose output
bru run --verbose
# Filter by tag
bru run --tag smoke-testsCI/CD integration
# .github/workflows/api-tests.yml
name: API Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Bruno CLI
run: npm install -g @usebruno/cli
- name: Run API Tests
run: |
cd api-collection
bru run --env staging --output results.json
env:
BRUNO_API_KEY: ${{ secrets.API_KEY }}
BRUNO_ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
- name: Upload Results
uses: actions/upload-artifact@v4
if: always()
with:
name: api-test-results
path: api-collection/results.jsonEnvironment variables in CI
# Export variables before running
export BRUNO_accessToken="your-token-here"
export BRUNO_apiKey="your-api-key"
# Bruno automatically uses variables with the BRUNO_ prefix
bru run --env productionAuthentication
Bearer Token
meta {
name: Protected Request
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/protected
body: none
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}Basic Auth
meta {
name: Basic Auth Request
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/data
body: none
auth: basic
}
auth:basic {
username: {{username}}
password: {{password}}
}API Key
meta {
name: API Key Request
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/data
body: none
auth: none
}
headers {
X-API-Key: {{apiKey}}
}OAuth2 flow
# 1. Login request
meta {
name: Login
type: http
seq: 1
}
post {
url: {{baseUrl}}/auth/login
body: json
auth: none
}
body:json {
{
"email": "{{email}}",
"password": "{{password}}"
}
}
script:post-response {
if (res.status === 200 && res.body.accessToken) {
bru.setVar("accessToken", res.body.accessToken);
bru.setVar("refreshToken", res.body.refreshToken);
bru.setVar("tokenExpiry", res.body.expiresIn);
}
}
tests {
test("login successful", function() {
expect(res.status).to.equal(200);
expect(res.body).to.have.property('accessToken');
});
}# 2. Refresh Token
meta {
name: Refresh Token
type: http
seq: 2
}
post {
url: {{baseUrl}}/auth/refresh
body: json
auth: none
}
body:json {
{
"refreshToken": "{{refreshToken}}"
}
}
script:post-response {
if (res.status === 200) {
bru.setVar("accessToken", res.body.accessToken);
}
}AWS Signature v4
meta {
name: AWS S3 Request
type: http
seq: 1
}
get {
url: {{awsUrl}}/bucket/object
body: none
auth: awsv4
}
auth:awsv4 {
accessKeyId: {{awsAccessKeyId}}
secretAccessKey: {{awsSecretAccessKey}}
region: {{awsRegion}}
service: s3
}Advanced usage
Collection-level scripts
// collection.bru - script executed for every request
script:pre-request {
// Add timestamp to every request
const timestamp = Date.now().toString();
req.setHeader('X-Request-Timestamp', timestamp);
// Log request info
console.log(`[${req.getMethod()}] ${req.getUrl()}`);
}
script:post-response {
// Log response info
console.log(`Response: ${res.status} in ${res.responseTime}ms`);
// Save to collection variables
if (res.headers['x-request-id']) {
bru.setVar('lastRequestId', res.headers['x-request-id']);
}
}Chaining requests
# 1. Create user
meta {
name: Create User
type: http
seq: 1
}
post {
url: {{baseUrl}}/api/users
body: json
auth: bearer
}
body:json {
{
"name": "Test User",
"email": "test@example.com"
}
}
script:post-response {
bru.setVar("createdUserId", res.body.id);
}# 2. Get created user (uses variable from previous request)
meta {
name: Get Created User
type: http
seq: 2
}
get {
url: {{baseUrl}}/api/users/{{createdUserId}}
body: none
auth: bearer
}
tests {
test("user exists", function() {
expect(res.status).to.equal(200);
expect(res.body.id).to.equal(bru.getVar("createdUserId"));
});
}Tagging and organization
meta {
name: Critical Health Check
type: http
seq: 1
}
docs {
This is a critical endpoint that should always be tested.
Used for monitoring and alerting.
}
get {
url: {{baseUrl}}/health
body: none
auth: none
}
assert {
res.status: eq 200
res.responseTime: lt 1000
}Assertions (alternative to tests)
assert {
res.status: eq 200
res.body.success: eq true
res.body.users: isArray
res.body.users.length: gt 0
res.responseTime: lt 500
}Best practices
Collection structure
api-collection/
├── bruno.json
├── README.md
├── environments/
│ ├── local.bru
│ ├── development.bru
│ ├── staging.bru
│ └── production.bru
├── auth/
│ ├── folder.bru
│ ├── login.bru
│ ├── logout.bru
│ ├── refresh.bru
│ └── register.bru
├── users/
│ ├── folder.bru
│ ├── crud/
│ │ ├── create.bru
│ │ ├── read.bru
│ │ ├── update.bru
│ │ └── delete.bru
│ └── admin/
│ └── list-all.bru
└── _shared/
├── health-check.bru
└── version.bruNaming conventions
# Requests
[HTTP_METHOD]-[resource]-[action].bru
# Examples
get-users.bru
get-user-by-id.bru
post-create-user.bru
put-update-user.bru
delete-user.bru
# Folders
lowercase-with-dashes/Secrets management
# .gitignore
environments/*.secret.bru
.env
.env.local
# Keep secrets outside the repository
# Use environment variables in CI/CDDocumentation in collection
meta {
name: Create Order
type: http
seq: 1
}
docs {
## Create a new order
Creates a new order in the system.
### Required fields:
- `items`: Array of items with productId and quantity
- `shippingAddress`: Delivery address object
### Response:
- `201 Created`: Order created successfully
- `400 Bad Request`: Invalid input data
- `401 Unauthorized`: Missing or invalid token
}
post {
url: {{baseUrl}}/api/orders
body: json
auth: bearer
}Integrations
VS Code extension
Bruno offers a VS Code extension for viewing and editing .bru files:
# Install from VS Code Marketplace
# Search for: "Bruno"Import from Postman
# In Bruno App:
# File > Import Collection > Postman Collection
# Supported formats:
# - Postman Collection v2.1
# - Postman Environment
# - OpenAPI/Swagger
# - InsomniaExport to OpenAPI
Bruno can export collections to OpenAPI/Swagger format for documentation purposes.
FAQ - frequently asked questions
How to migrate from Postman?
- In Postman: Export Collection (Collection v2.1)
- In Bruno: File > Import Collection
- Select the .json file
- Review and adjust environment variables
Does Bruno support team collaboration?
Yes, through Git. The entire team can work on the same collection:
- Everyone has a local copy
- Changes are merged through Git
- Code review for API changes
How to use Bruno with CI/CD?
# Install CLI in your pipeline
npm install -g @usebruno/cli
# Run tests
bru run --env staging
# With output for reports
bru run --output results.jsonCan I use npm libraries in scripts?
Bruno has some built-in libraries:
crypto- cryptographyuuid- UUID generation- Standard JavaScript APIs
For more advanced needs, consider external pre-processing.
How to debug scripts?
// Use console.log
script:pre-request {
console.log("Request URL:", req.getUrl());
console.log("Headers:", JSON.stringify(req.getHeaders(), null, 2));
console.log("Variables:", {
baseUrl: bru.getVar("baseUrl"),
token: bru.getVar("accessToken")
});
}Summary
Bruno is an excellent Postman alternative for teams that:
- Use Git - Collections as source code
- Value privacy - Data stays local
- Need offline access - Full functionality without internet
- Prefer open-source - MIT License, active community
- Want to save money - Everything is free
Bruno makes API testing a part of the normal development workflow, with code review, version control, and CI/CD integration.