Bruno Collection Generator
Generate Bruno collection files for the open-source, Git-friendly API client.
Core Workflow Scan routes: Find all API route definitions Extract metadata: Methods, paths, params, bodies Create collection: Initialize bruno.json manifest Generate .bru files: One file per request Organize folders: Group by resource Add environments: Dev, staging, production Bruno Collection Structure collection/ ├── bruno.json # Collection manifest ├── environments/ │ ├── Development.bru │ ├── Staging.bru │ └── Production.bru ├── users/ │ ├── folder.bru │ ├── get-users.bru │ ├── get-user.bru │ ├── create-user.bru │ ├── update-user.bru │ └── delete-user.bru ├── auth/ │ ├── folder.bru │ ├── login.bru │ ├── register.bru │ └── logout.bru └── products/ ├── folder.bru └── ...
bruno.json Manifest { "version": "1", "name": "My API", "type": "collection", "ignore": ["node_modules", ".git"] }
.bru File Syntax meta { name: Get Users type: http seq: 1 }
get { url: {{baseUrl}}/users body: none auth: bearer }
auth:bearer { token: {{authToken}} }
query { page: 1 limit: 10 }
headers { Accept: application/json }
docs { Retrieve a paginated list of users. }
Generator Script // scripts/generate-bruno.ts import * as fs from "fs"; import * as path from "path";
interface RouteInfo { method: string; path: string; name: string; description?: string; body?: object; queryParams?: { name: string; value: string }[]; auth?: boolean; }
interface BrunoOptions { collectionName: string; outputDir: string; baseUrl: string; authType?: "bearer" | "basic" | "apikey"; }
function generateBrunoCollection( routes: RouteInfo[], options: BrunoOptions ): void { const { outputDir, collectionName } = options;
// Create output directory if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); }
// Create bruno.json const manifest = { version: "1", name: collectionName, type: "collection", ignore: ["node_modules", ".git"], }; fs.writeFileSync( path.join(outputDir, "bruno.json"), JSON.stringify(manifest, null, 2) );
// Create environments generateEnvironments(outputDir, options);
// Group routes by resource const groupedRoutes = groupRoutesByResource(routes);
for (const [resource, resourceRoutes] of Object.entries(groupedRoutes)) { const folderPath = path.join(outputDir, resource);
if (!fs.existsSync(folderPath)) {
fs.mkdirSync(folderPath, { recursive: true });
}
// Create folder.bru
const folderBru = `meta {\n name: ${capitalize(resource)}\n}\n`;
fs.writeFileSync(path.join(folderPath, "folder.bru"), folderBru);
// Create request files
let seq = 1;
for (const route of resourceRoutes) {
const fileName = generateFileName(route);
const content = generateBruFile(route, seq++, options);
fs.writeFileSync(path.join(folderPath, `${fileName}.bru`), content);
}
} }
function generateBruFile( route: RouteInfo, seq: number, options: BrunoOptions ): string { const lines: string[] = [];
// Meta section
lines.push("meta {");
lines.push(name: ${route.name});
lines.push(" type: http");
lines.push(seq: ${seq});
lines.push("}");
lines.push("");
// Request section const method = route.method.toLowerCase(); const urlPath = route.path.replace(/:(\w+)/g, "{{$1}}");
lines.push(${method} {);
lines.push(url: {{baseUrl}}${urlPath});
if (["post", "put", "patch"].includes(method) && route.body) { lines.push(" body: json"); } else { lines.push(" body: none"); }
if (route.auth && options.authType) {
lines.push(auth: ${options.authType});
} else {
lines.push(" auth: none");
}
lines.push("}"); lines.push("");
// Auth section if (route.auth && options.authType === "bearer") { lines.push("auth:bearer {"); lines.push(" token: {{authToken}}"); lines.push("}"); lines.push(""); } else if (route.auth && options.authType === "basic") { lines.push("auth:basic {"); lines.push(" username: {{username}}"); lines.push(" password: {{password}}"); lines.push("}"); lines.push(""); }
// Query params
if (route.queryParams?.length) {
lines.push("query {");
for (const param of route.queryParams) {
lines.push(${param.name}: ${param.value});
}
lines.push("}");
lines.push("");
}
// Headers lines.push("headers {"); lines.push(" Accept: application/json"); if (["post", "put", "patch"].includes(method)) { lines.push(" Content-Type: application/json"); } lines.push("}"); lines.push("");
// Body if (["post", "put", "patch"].includes(method) && route.body) { lines.push("body:json {"); lines.push(JSON.stringify(route.body, null, 2)); lines.push("}"); lines.push(""); }
// Docs
if (route.description) {
lines.push("docs {");
lines.push(${route.description});
lines.push("}");
}
return lines.join("\n"); }
function generateEnvironments(outputDir: string, options: BrunoOptions): void { const envsDir = path.join(outputDir, "environments");
if (!fs.existsSync(envsDir)) { fs.mkdirSync(envsDir, { recursive: true }); }
const environments = [ { name: "Development", baseUrl: "http://localhost:3000/api" }, { name: "Staging", baseUrl: "https://staging-api.example.com" }, { name: "Production", baseUrl: "https://api.example.com" }, ];
for (const env of environments) { const content = `vars { baseUrl: ${env.baseUrl} authToken: }
vars:secret [
authToken
]
;
fs.writeFileSync(path.join(envsDir,${env.name}.bru`), content);
}
}
function generateFileName(route: RouteInfo): string { return route.name.toLowerCase().replace(/\s+/g, "-"); }
function groupRoutesByResource(
routes: RouteInfo[]
): Record
for (const route of routes) { const parts = route.path.split("/").filter(Boolean); const resource = parts[0] || "api";
if (!groups[resource]) {
groups[resource] = [];
}
groups[resource].push(route);
}
return groups; }
function capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1); }
Complete Example Files bruno.json { "version": "1", "name": "My API", "type": "collection", "ignore": ["node_modules", ".git"] }
environments/Development.bru vars { baseUrl: http://localhost:3000/api authToken: userId: 1 }
vars:secret [ authToken ]
environments/Production.bru vars { baseUrl: https://api.example.com authToken: userId: }
vars:secret [ authToken ]
users/folder.bru meta { name: Users }
users/get-users.bru meta { name: Get Users type: http seq: 1 }
get { url: {{baseUrl}}/users body: none auth: bearer }
auth:bearer { token: {{authToken}} }
query { page: 1 limit: 10 }
headers { Accept: application/json }
docs { Retrieve a paginated list of users.
## Query Parameters - page: Page number (default: 1) - limit: Items per page (default: 10, max: 100)
## Response Returns paginated user list with metadata. }
users/get-user.bru meta { name: Get User by ID type: http seq: 2 }
get { url: {{baseUrl}}/users/{{userId}} body: none auth: bearer }
auth:bearer { token: {{authToken}} }
headers { Accept: application/json }
docs { Retrieve a single user by their ID. }
users/create-user.bru meta { name: Create User type: http seq: 3 }
post { url: {{baseUrl}}/users body: json auth: bearer }
auth:bearer { token: {{authToken}} }
headers { Accept: application/json Content-Type: application/json }
body:json { { "name": "John Doe", "email": "john@example.com", "role": "user" } }
docs { Create a new user account.
## Request Body - name: User's full name (required) - email: User's email address (required, unique) - role: User role (optional, default: "user") }
users/update-user.bru meta { name: Update User type: http seq: 4 }
put { url: {{baseUrl}}/users/{{userId}} body: json auth: bearer }
auth:bearer { token: {{authToken}} }
headers { Accept: application/json Content-Type: application/json }
body:json { { "name": "John Updated", "email": "john.updated@example.com" } }
docs { Update an existing user. }
users/delete-user.bru meta { name: Delete User type: http seq: 5 }
delete { url: {{baseUrl}}/users/{{userId}} body: none auth: bearer }
auth:bearer { token: {{authToken}} }
headers { Accept: application/json }
docs { Delete a user account. }
auth/login.bru meta { name: Login type: http seq: 1 }
post { url: {{baseUrl}}/auth/login body: json auth: none }
headers { Accept: application/json Content-Type: application/json }
body:json { { "email": "user@example.com", "password": "password123" } }
script:post-response { if (res.body.token) { bru.setEnvVar("authToken", res.body.token); } }
docs { Authenticate user and receive access token.
On successful login, the token is automatically saved to the authToken environment variable. }
Pre/Post Request Scripts
script:pre-request {
// Set dynamic values before request
const timestamp = Date.now();
bru.setVar("requestId", req-${timestamp});
}
script:post-response { // Extract values from response if (res.body.token) { bru.setEnvVar("authToken", res.body.token); }
if (res.body.id) { bru.setEnvVar("userId", res.body.id); }
// Log response info
console.log(Status: ${res.status});
console.log(Response time: ${res.responseTime}ms);
}
Tests in Bruno tests { test("should return 200", function() { expect(res.status).to.equal(200); });
test("should return array of users", function() { expect(res.body.data).to.be.an("array"); });
test("should include pagination", function() { expect(res.body.meta).to.have.property("page"); expect(res.body.meta).to.have.property("total"); }); }
CLI Script
!/usr/bin/env node
// scripts/bruno-gen.ts import * as fs from "fs"; import { program } from "commander";
program
.name("bruno-gen")
.description("Generate Bruno collection from API routes")
.option("-f, --framework
const options = program.opts();
async function main() { const routes = await scanRoutes(options.framework, options.source);
generateBrunoCollection(routes, { collectionName: options.name, outputDir: options.output, baseUrl: options.baseUrl, authType: options.auth, });
console.log(Generated Bruno collection in ${options.output});
console.log(Open with: bruno run ${options.output});
}
main();
Best Practices Git-friendly: Bruno stores everything as plain text files Use environments: Store URLs and tokens in environment files Secret variables: Mark sensitive vars with vars:secret Add docs: Document each request with the docs block Pre/post scripts: Automate token extraction and setup Add tests: Include assertions in test blocks Organize folders: Group related requests together Sequence numbers: Order requests logically with seq Output Checklist bruno.json manifest created Environment files for dev/staging/prod Folder structure by resource folder.bru for each folder Request .bru files with proper syntax Path parameters use {{param}} syntax Query parameters in query block Request bodies in body:json block Authentication configured Documentation in docs block Pre/post scripts for token handling