Skip to main content
skill CX Codex

bruno-collection-generator — Claude Code skill: Bruno collections

This skill scans Express, Next.js or Fastify routes and turns them into a Bruno collection: one .bru file per request, with environments and auth.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add patricio0312rev/skills --skill bruno-collection-generator -a claude-code
Open on GitHub

What bruno-collection-generator does

The skill walks your route definitions, pulls out methods, paths, params and request bodies, then builds a collection for Bruno, the open-source API client that keeps requests as plain text and plays well with Git. You get a bruno.json manifest, folders grouped by resource with a folder.bru inside each, and a separate .bru file per request.

The workflow is spelled out step by step: scan routes, extract metadata, initialize the collection, generate files, group by resource, add environments. Development, Staging and Production come out of the box, each with its own baseUrl. Auth covers bearer, basic and apikey, with the token passed as {{authToken}}. There is also a TypeScript generator at scripts/generate-bruno.ts if you would rather run the whole thing programmatically.

That script expects Node.js, since it writes files through fs and path. Routes declared in an unusual way may not parse cleanly, so expect to fix a few files by hand.

What it helps with

  • turn existing API routes into a Bruno collection
  • group requests into resource folders instead of one flat list
  • set up Development, Staging and Production environments with separate baseUrl values
  • apply bearer auth across every protected request
  • generate .bru files from a TypeScript script

Example prompts for the agent

  • generate a bruno collection from this project's routes
  • create .bru files for all users and auth endpoints
  • add Development, Staging and Production environments to the Bruno collection

How to install bruno-collection-generator

1Skills CLI (Vercel)

npx skills add patricio0312rev/skills --skill bruno-collection-generator -a claude-code

2Manually: put the files in place

~/.claude/skills/bruno-collection-generator/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/bruno-collection-generator/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about bruno-collection-generator

How do I install bruno-collection-generator in Claude?

Run npx skills add patricio0312rev/skills --skill bruno-collection-generator -a claude-code. The skill file then lands at ~/.claude/skills/bruno-collection-generator/SKILL.md.

What does bruno-collection-generator do?

It finds API route definitions in Express, Next.js, Fastify and other frameworks and converts them into a Bruno collection: manifest, resource folders, request files and environments.

Which auth types does the skill support?

Bearer, basic and apikey. Bearer requests use the {{authToken}} variable, while basic auth uses {{username}} and {{password}}.

Source text

SKILL.md · MIT
---
name: bruno-collection-generator
description: Generates Bruno collection files (.bru) from Express, Next.js, Fastify, or other API routes. Creates organized collections with environments, authentication, and folder structure for the open-source Bruno API client. Use when users request "generate bruno collection", "bruno api testing", "create bru files", or "bruno import".
---

# Bruno Collection Generator

Generate Bruno collection files for the open-source, Git-friendly API client.

## Core Workflow

1. **Scan routes**: Find all API route definitions
2. **Extract metadata**: Methods, paths, params, bodies
3. **Create collection**: Initialize bruno.json manifest
4. **Generate .bru files**: One file per request
5. **Organize folders**: Group by resource
6. **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

```json
{
  "version": "1",
  "name": "My API",
  "type": "collection",
  "ignore": ["node_modules", ".git"]
}
```

## .bru File Syntax

```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.
}
```

## Generator Script

```typescript
// 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<string, RouteInfo[]> {
  const groups: Record<string, RouteInfo[]> = {};

  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

```json
{
  "version": "1",
  "name": "My API",
  "type": "collection",
  "ignore": ["node_modules", ".git"]
}
```

### environments/Development.bru

```bru
vars {
  baseUrl: http://localhost:3000/api
  authToken:
  userId: 1
}

vars:secret [
  authToken
]
```

### environments/Production.bru

```bru
vars {
  baseUrl: https://api.example.com
  authToken:
  userId:
}

vars:secret [
  authToken
]
```

### users/folder.bru

```bru
meta {
  name: Users
}
```

### users/get-users.bru

```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

```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

```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": "[email protected]",
    "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

```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": "[email protected]"
  }
}

docs {
  Update an existing user.
}
```

### users/delete-user.bru

```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

```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": "[email protected]",
    "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

```bru
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

```bru
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

```typescript
#!/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 <type>", "Framework type", "express")
  .option("-s, --source <path>", "Source directory", "./src")
  .option("-o, --output <path>", "Output directory", "./bruno-collection")
  .option("-n, --name <name>", "Collection name", "My API")
  .option("-b, --base-url <url>", "Base URL", "http://localhost:3000/api")
  .option("-a, --auth <type>", "Auth type (bearer|basic|apikey)")
  .parse();

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

1. **Git-friendly**: Bruno stores everything as plain text files
2. **Use environments**: Store URLs and tokens in environment files
3. **Secret variables**: Mark sensitive vars with `vars:secret`
4. **Add docs**: Document each request with the docs block
5. **Pre/post scripts**: Automate token extraction and setup
6. **Add tests**: Include assertions in test blocks
7. **Organize folders**: Group related requests together
8. **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

© patricio0312rev · MIT · patricio0312rev/skills

Similar skills

Everything for Testing and QA →

From the same set26

framer-motion-animator Generates smooth Framer Motion animations and micro-interactions in React. Useful for building page transitions, drag gestures, and scroll effects. CX Codex +1 11,100installs eslint-prettier-config Sets up ESLint and Prettier in a TypeScript and React project: configs, rules, scripts and a pre-commit hook. CX Codex +1 1,100installs changelog-writer Turns git commits, PR titles and issue references into a CHANGELOG and release notes. Useful right before a release, when changes need sorting and a version number. CX Codex +1 615installs jsdoc-typescript-docs A skill that writes JSDoc comments across a TypeScript codebase and builds docs from them with TypeDoc. Handy when functions have piled up without descriptions. CX Codex +1 547installs dockerfile-optimizer A skill that rewrites your Dockerfile around multi-stage builds: smaller images, faster builds, working layer cache. Useful when the image has bloated or CI drags. CX Codex +1 530installs animation-micro-interaction-pack A set of reusable motion patterns for interfaces: hover effects, entrances and exits, gesture feedback. Useful when a UI feels static and you want it to move without hand-tuning timings. CX Codex +1 499installs explaining-code Walks through unfamiliar code in steps: an everyday analogy, an ASCII diagram of the flow, then a line-by-line breakdown. Handy when you inherit a repo. CX Codex +1 473installs conventional-commits The skill writes commit messages to the Conventional Commits spec: type, scope, body and footers. Useful when you want automated versioning and changelogs. CX Codex +1 469installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor