
Introduction 🎯
Every API needs documentation. The question is how you build it — and keep it from rotting.
Hand-written docs look great on day one. By day ninety, the POST /users endpoint accepts a displayName field but the docs still say name. The response example shows a 201 with { "id": 1 } but the real API returns { "userId": "uuid-here" }. Someone on the team quips “the docs are aspirational” and everyone laughs nervously.
Spec-driven documentation solves this. You write one canonical OpenAPI specification (OAS) — a JSON/YAML file that describes every endpoint, parameter, response, and error code — and let a documentation tool render it as a beautiful reference site. The spec is the source of truth. When the API changes, you update the spec, and the docs update automatically.
In this article, we’ll study the bKash Developer Reference as our design target — its sidebar navigation, endpoint-per-page layout, parameter tables, sample requests with tabbed code examples, and response payloads — and build equivalent documentation from an OpenAPI spec using the best open-source tools available.
# A fragment of an OpenAPI spec — this IS the documentation source
paths:
/tokenized/checkout/create:
post:
summary: Create a payment
description: Initiates a tokenized checkout payment request.
parameters:
- name: X-App-Key
in: header
required: true
schema: { type: string }
requestBody:
content:
application/json:
schema:
type: object
properties:
mode:
type: string
example: "0001"
amount:
type: string
example: "500"
No separate docs site. No copy-paste. No async drift. The spec is the documentation. 🚀
Part 1: Why Spec-Driven Documentation?
1.1 The Hand-Written Docs Trap
Most teams start with good intentions. They write markdown, host it on a wiki or a static site, and manually update it when the API changes. Here’s what happens next:
| Pain Point | Hand-Written | Spec-Driven |
|---|---|---|
| Sync | Docs lag behind code by days/weeks | Spec IS the source of truth |
| Accuracy | Copy-paste errors accumulate | Auto-generated from spec |
| Effort per endpoint | Write docs separately | Add description to spec |
| Request/response examples | Manually crafted | Generated from spec schemas |
| Schema drift | Undetected until a customer complains | CI validates spec against implementation |
| Multi-language examples | Write each one manually | Auto-generated (curl, JS, Python, etc.) |
| Versioning | Separate doc copies per version | Versioned alongside the spec file |
1.2 The bKash Reference Style as a Target
The bKash Developer Reference (powered by ReadMe) exemplifies the standard that API consumers expect:
┌──────────────────────────────────────────────┐
│ Navigation │ Endpoint │
│ ┌──────────────────┐ │ Content │
│ │ Quick Start │ │ │
│ │ ▼ Tokenized │ │ Description │
│ │ ├ Grant Token │ │ Request URL │
│ │ ├ Create Pmt │ │ │
│ │ ├ Execute Pmt │ │ Headers Table │
│ │ └ Refund │ │ │
│ │ ▼ Checkout URL │ │ Params Table │
│ │ ▼ Auth & Capture │ │ │
│ │ ▼ Refund │ │ Sample Req │
│ │ ▼ Webhooks │ │ ┌─────┬─────┐ │
│ │ Error Codes │ │ │JSON │cURL │ │
│ └──────────────────┘ │ └─────┴─────┘ │
│ │ │
│ │ Sample Resp │
│ │ │
│ │ Error Codes │
└──────────────────────────────┴────────────────┘
Key design elements we want to replicate:
- Left sidebar with grouped, hierarchical navigation
- Right content: description, URL, headers table, params table, request samples (tabbed), response samples, error codes
- Parameter tables with Name, Type, Required, Description columns
- Schema-aware — types and formats come from the spec, not manual markup
- Responsive — readable on desktop and mobile
Part 2: The OpenAPI Specification — Your Documentation Source
Before generating docs, you need a well-structured OpenAPI spec. A complete spec has four top-level sections that map directly to documentation pages.
2.1 Anatomy of an OpenAPI Spec
openapi: 3.0.3
info:
title: bKash Tokenized Checkout API
description: API specification for bKash Tokenized Checkout integration.
version: 1.2.0-beta
servers:
- url: https://sandbox.bkash.com
description: Sandbox environment
- url: https://api.bkash.com
description: Production environment
paths:
/tokenized/checkout/token/grant:
post:
summary: Grant an access token
description: Obtain an access token for API authentication.
tags: [Authentication]
parameters: [...]
requestBody: { ... }
responses:
'200':
description: Token granted successfully
content:
application/json:
schema:
type: object
properties:
id_token:
type: string
description: Access token for subsequent API calls.
expires_in:
type: integer
description: Token lifetime in seconds.
components:
schemas:
PaymentRequest:
type: object
properties:
amount:
type: string
description: Payment amount
currency:
type: string
enum: [BDT]
ErrorResponse:
type: object
properties:
errorCode:
type: string
errorMessage:
type: string
Each section serves a documentation purpose:
| Spec Section | Docs Page Element |
|---|---|
info | Page title, description, version badge |
servers | Base URL selector / environment tabs |
paths | One page per endpoint group (by tag) |
paths.*.summary | Endpoint title in sidebar |
paths.*.parameters | Request parameters table |
paths.*.requestBody | Request body schema |
paths.*.responses | Response schemas + status codes |
components.schemas | Reusable type definitions |
tags | Sidebar grouping + navigation order |
2.2 Tag Grouping Drives Navigation Structure
The tags array and the tags on each path determine how your sidebar looks:
# Tag definitions (appear in sidebar in this order)
tags:
- name: Authentication
description: Token management endpoints
- name: Payments
description: Payment creation and execution
- name: Refunds
description: Refund operations
# Each path references a tag
paths:
/tokenized/checkout/token/grant:
post:
tags: [Authentication]
summary: Grant Token
/tokenized/checkout/create:
post:
tags: [Payments]
summary: Create Payment
/tokenized/checkout/execute:
post:
tags: [Payments]
summary: Execute Payment
This produces a sidebar exactly like bKash’s:
▼ Authentication
Grant Token
Refresh Token
▼ Payments
Create Payment
Execute Payment
▼ Refunds
Refund Transaction
Refund Status
Part 3: Tool Comparison — Choosing Your Docs Engine
Several open-source tools render OpenAPI specs into HTML. Here’s how they compare against the bKash reference style:
| Feature | Redoc (Redocly) | Swagger UI | Stoplight Elements | RapiDoc | Scalar |
|---|---|---|---|---|---|
| Sidebar nav | ✅ Tree by tags | ⚠️ Flat list | ✅ Tree by tags | ✅ Tree by tags | ✅ Tree by tags |
| Parameter tables | ✅ Rich | ✅ Tables | ✅ Rich | ✅ Tables | ✅ Rich |
| Schema rendering | ✅ Deeply nested | ⚠️ Basic | ✅ Collapsible | ✅ Collapsible | ✅ Collapsible |
| Try-it console | ❌ (Redoc only) | ✅ Built-in | ✅ Built-in | ✅ Built-in | ✅ Built-in |
| Tabbed examples | ✅ Yes | ✅ Yes | ✅ Yes | ⚠️ Limited | ✅ Yes |
| Response samples | ✅ Auto from schema | ✅ Auto | ✅ Auto | ✅ Auto | ✅ Auto |
| Dark mode | ✅ Built-in | ⚠️ Themed | ✅ Built-in | ✅ Built-in | ✅ Built-in |
| Code sample gen | ❌ | ⚠️ Basic (curl) | ❌ | ❌ | ✅ Multi-lang |
| Search | ⚠️ Plugin | ❌ | ✅ Built-in | ✅ Built-in | ✅ Built-in |
| React component | ✅ Redoc/RedocStandalone | ✅ SwaggerUI | ✅ APIReference | ✅ RapiDoc (WC) | ✅ Scalar (WC) |
| CI/CD friendly | ✅ CLI + bundle | ✅ npm | ✅ npm | ✅ npm | ✅ npm |
| License | MIT | Apache 2.0 | Apache 2.0 | MIT | MIT |
| Style closest to bKash? | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
3.1 Redoc — Best for Static Reference Pages
Redoc generates a clean, two-panel layout (sidebar + content) that closely mirrors the bKash reference style. It’s the most popular choice for standalone API reference sites.
# Install Redocly CLI
npm install -g @redocly/cli
# Generate a static HTML page from your spec
redocly build-docs openapi.yaml -o docs/index.html
The output is a single self-contained HTML file. No server, no build step, no JavaScript framework. Drop it behind any web server and it works.
3.2 Swagger UI — Best for Try-It Playgrounds
Swagger UI adds an interactive “Try it out” button on every endpoint — users can send real requests from the browser. This is ideal for sandbox environments but less clean as a reference-only docs site.
3.3 Stoplight Elements — Best for the bKash Look
Stoplight Elements provides a React component called APIReference that feels closest to the bKash/ReadMe aesthetic — clean parameter tables, collapsible schemas, and a sidebar that groups by tags.
import { APIReference } from '@stoplight/elements'
import spec from './openapi.json'
function ApiDocs() {
return (
<APIReference
apiDescriptionDocument={spec}
layout="sidebar"
router="history"
/>
)
}
3.4 Scalar — Best Modern Alternative
Scalar offers a polished, modern docs experience with dark mode, multi-language code samples (curl, JS, Python, Go, etc.), and a clean sidebar layout. It’s the newest contender and gaining rapid adoption.
npm install @scalar/api-reference
import { ApiReference } from '@scalar/api-reference'
function App() {
return <ApiReference configuration={{ spec: { url: '/openapi.yaml' } }} />
}
Part 4: Building a Docs Site — Step by Step
Let’s build a complete documentation site using Redoc (for the static reference) and enhance it with Select Dropdown for environment switching.
4.1 Project Structure
api-docs/
├── spec/
│ ├── openapi.yaml # Main spec (references fragments)
│ ├── fragments/
│ │ ├── info.yaml
│ │ ├── servers.yaml
│ │ ├── paths/
│ │ │ ├── auth.yaml
│ │ │ ├── payments.yaml
│ │ │ └── refunds.yaml
│ │ └── components/
│ │ ├── schemas.yaml
│ │ └── responses.yaml
│ └── examples/
│ ├── payment-request.json
│ └── payment-response.json
├── src/
│ ├── index.html
│ ├── styles/
│ │ └── theme.css # Custom theming
│ └── scripts/
│ └── env-selector.js # Server URL switcher
├── redocly.yaml # Redocly config
├── package.json
└── .github/
└── workflows/
└── deploy-docs.yml
4.2 Define Your Spec with Tag Grouping
# spec/openapi.yaml
openapi: 3.0.3
info:
title: Merchant Payment API
description: |
# Merchant Payment API
Complete API reference for processing tokenized payments,
managing refunds, and handling webhook notifications.
### Base URLs
| Environment | URL |
|---|---|
| Sandbox | `https://sandbox-api.merchant.com` |
| Production | `https://api.merchant.com` |
version: 1.0.0
servers:
- url: https://sandbox-api.merchant.com
description: Sandbox
- url: https://api.merchant.com
description: Production
tags:
- name: Authentication
description: Obtain and refresh API tokens
x-displayName: Authentication
- name: Payments
description: Create, execute, and query payments
x-displayName: Payments
- name: Refunds
description: Process and check refunds
x-displayName: Refunds
- name: Webhooks
description: Receive real-time payment notifications
x-displayName: Webhooks
paths:
/token/grant:
post:
tags: [Authentication]
summary: Grant Token
description: |
Obtain an access token for authenticating subsequent API calls.
The token expires in 3600 seconds. Use the Refresh Token endpoint
before expiry to get a new token without re-authenticating.
operationId: grantToken
parameters:
- name: username
in: header
required: true
schema:
type: string
description: Merchant account username (provided during onboarding)
- name: password
in: header
required: true
schema:
type: string
description: Merchant account password
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [app_key, app_secret]
properties:
app_key:
type: string
description: Application key shared during onboarding
example: test_app_key
app_secret:
type: string
description: Application secret shared during onboarding
example: test_app_secret
responses:
'200':
description: Token granted successfully
content:
application/json:
schema:
type: object
properties:
id_token:
type: string
description: Access token for subsequent API calls
example: eyJhbGciOiJIUzI1NiIs...
expires_in:
type: integer
description: Token lifetime in seconds
example: 3600
token_type:
type: string
example: Bearer
refresh_token:
type: string
description: Token for refreshing before expiry
example: eyJhbGciOiJIUzI1NiIs...
'401':
$ref: '#/components/responses/Unauthorized'
/payment/create:
post:
tags: [Payments]
summary: Create Payment
description: |
Initiate a payment request. Returns a `bkashURL` where the customer
must be redirected to complete authentication.
The payment ID expires after 24 hours if not executed.
operationId: createPayment
parameters:
- name: X-App-Key
in: header
required: true
schema:
type: string
description: Application key shared during onboarding
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token from Grant Token API
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePaymentRequest'
responses:
'200':
description: Payment initiated
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePaymentResponse'
'400':
$ref: '#/components/responses/BadRequest'
components:
schemas:
CreatePaymentRequest:
type: object
required: [mode, payerReference, callbackURL, amount, currency, intent, merchantInvoiceNumber]
properties:
mode:
type: string
description: Payment mode
enum: ['0001', '0011']
example: '0001'
payerReference:
type: string
description: Customer wallet number or reference
maxLength: 255
example: '01712345678'
callbackURL:
type: string
format: uri
description: Base URL for success/failure/cancel callbacks
example: https://merchant.com/callback
amount:
type: string
description: Payment amount
example: '1500'
currency:
type: string
enum: [BDT]
default: BDT
intent:
type: string
enum: [sale, authorization]
description: 'sale for immediate capture, authorization for hold'
merchantInvoiceNumber:
type: string
maxLength: 255
description: Unique merchant-side invoice number
example: INV-20260720-001
CreatePaymentResponse:
type: object
properties:
statusCode:
type: string
example: '0000'
statusMessage:
type: string
example: Successful
paymentID:
type: string
description: bKash-generated payment ID (expires in 24h)
example: TR0011dQPHnuY1720518383420
transactionStatus:
type: string
enum: [Initiated, Completed, Failed]
bkashURL:
type: string
format: uri
description: Redirect the customer to this URL for PIN entry
amount:
type: string
currency:
type: string
enum: [BDT]
merchantInvoiceNumber:
type: string
ErrorResponse:
type: object
properties:
errorCode:
type: string
description: Numeric error code
example: '2001'
errorMessage:
type: string
description: Human-readable error description
example: Invalid App Key
responses:
Unauthorized:
description: Authentication failed
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
BadRequest:
description: Invalid request parameters
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
4.3 Generate the Docs with Redoc
# Option A: Single HTML file (simplest)
npx @redocly/cli build-docs spec/openapi.yaml \
-o docs/index.html
# Option B: With custom theme
npx @redocly/cli build-docs spec/openapi.yaml \
-o docs/index.html \
--theme.openapi.logo.url /logo.svg \
--theme.openapi.colors.primary.main '#1a56db'
4.4 Custom Theme (Redocly)
# redocly.yaml
theme:
openapi:
logo:
url: https://merchant.com/logo.svg
altText: Merchant API Docs
colors:
primary:
main: '#0F172A' # Dark navy — like bKash's theme
success:
main: '#10B981'
warning:
main: '#F59E0B'
error:
main: '#EF4444'
typography:
fontFamily: 'Inter, system-ui, sans-serif'
headings:
fontFamily: 'Inter, system-ui, sans-serif'
fontWeight: '600'
code:
fontFamily: '"JetBrains Mono", "Fira Code", monospace'
fontSize: '13px'
sidebar:
backgroundColor: '#F8FAFC'
activeTextColor: '#0F172A'
textColor: '#475569'
rightPanel:
backgroundColor: '#0F172A' # Dark panel for request/response samples
4.5 Environment Selector
Add a dropdown to switch between base URLs — a feature every consumer-grade API reference needs:
<!-- src/index.html (wrapping Redoc output) -->
<!DOCTYPE html>
<html>
<head>
<title>Merchant Payment API Reference</title>
<style>
/* Match Redoc's theme */
:root {
--env-selector-bg: #F8FAFC;
--env-selector-border: #E2E8F0;
}
#env-selector {
position: fixed;
top: 16px;
right: 24px;
z-index: 100;
padding: 6px 12px;
border: 1px solid var(--env-selector-border);
border-radius: 6px;
background: var(--env-selector-bg);
font-size: 13px;
font-family: 'Inter', system-ui, sans-serif;
}
</style>
</head>
<body>
<select id="env-selector">
<option value="https://sandbox-api.merchant.com">Sandbox</option>
<option value="https://api.merchant.com">Production</option>
</select>
<div id="redoc-container"></div>
<script>
// Redoc renders here
// Environment selector updates base URL
const envSelector = document.getElementById('env-selector')
// Store preference
envSelector.addEventListener('change', () => {
localStorage.setItem('api-env', envSelector.value)
})
// Restore preference
const saved = localStorage.getItem('api-env')
if (saved) envSelector.value = saved
</script>
</body>
</html>
Part 5: Advanced Layouts — Picking Your Stack
5.1 Single Static HTML (Redoc)
Best for: Pure reference docs, no interactivity needed.
npx @redocly/cli build-docs spec.yaml -o docs/index.html
# Deploy docs/ to any static host (Netlify, Vercel, GitHub Pages, S3, etc.)
Pros: One file, any host, zero maintenance. Cons: No try-it console, no search (without plugin), no theming beyond Redocly’s options.
5.2 React SPA (Stoplight Elements)
Best for: Full control, custom pages, authentication flows, try-it console.
npm create vite@latest api-docs-site -- --template react-ts
npm install @stoplight/elements
// src/App.tsx
import { APIReference } from '@stoplight/elements'
import '@stoplight/elements/styles.min.css'
const specUrl = import.meta.env.PROD
? '/api/v1/openapi.yaml' // Serve from your API server
: '/openapi.yaml' // Local dev file
function App() {
return (
<div className="api-docs">
<APIReference
apiDescriptionDocument={specUrl}
layout="sidebar"
router="history"
basePath="/docs"
/>
</div>
)
}
export default App
npm run build
# Deploy dist/ to any static host
Pros: Full React ecosystem, custom pages, search built-in, try-it console. Cons: Heavier bundle, requires a build step.
5.3 Static Site Generator (Docusaurus + Redoc)
Best for: Documentation sites with guides + reference, multiple spec versions.
npx create-docusaurus@latest docs-site classic
npm install docusaurus-plugin-redoc
// docusaurus.config.ts
export default {
plugins: [
[
'docusaurus-plugin-redoc',
{
specPath: 'spec/openapi.yaml',
routePath: '/api/',
enableSearch: true,
theme: {
colors: { primary: { main: '#0F172A' } },
},
},
],
],
}
Now you have:
/docs/— guides and tutorials (markdown)/api/— spec-driven reference (Redoc)
Pros: Best of both worlds — guides + reference, versioning, search across all content. Cons: More setup, Docusaurus learning curve.
5.4 Vue/Nuxt Integration (Scalar)
npm create nuxt@latest api-docs
npm install @scalar/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@scalar/nuxt'],
scalar: {
spec: {
url: '/openapi.yaml',
},
theme: 'purple',
},
})
Part 6: CI/CD Pipeline
6.1 Validate Spec on Every Commit
# .github/workflows/spec-validation.yml
name: Validate OpenAPI Spec
on:
pull_request:
paths:
- 'spec/**/*.yaml'
- 'spec/**/*.json'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Redocly CLI
run: npm install -g @redocly/cli
- name: Lint spec
run: redocly lint spec/openapi.yaml
- name: Validate spec
run: redocly validate spec/openapi.yaml
6.2 Build and Deploy Docs
# .github/workflows/deploy-docs.yml
name: Deploy API Docs
on:
push:
branches: [main]
paths:
- 'spec/**'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build docs
run: |
npm install -g @redocly/cli
redocly build-docs spec/openapi.yaml \
--theme.openapi.colors.primary.main '#0F172A' \
-o docs/index.html
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
publish_branch: gh-pages
6.3 Spec ↔ Code Contract Testing
To ensure your spec stays in sync with your actual API, add contract testing:
// tests/spec-contract.test.ts
import { test, expect } from 'vitest'
import axios from 'axios'
import fs from 'fs'
import { parse } from 'yaml'
const spec = parse(fs.readFileSync('spec/openapi.yaml', 'utf-8'))
test('all endpoints in spec are reachable', async () => {
for (const [path, methods] of Object.entries(spec.paths)) {
for (const method of Object.keys(methods)) {
if (['get', 'post', 'put', 'patch', 'delete'].includes(method)) {
const url = `http://localhost:3000${path}`
const res = await axios({ method, url, validateStatus: () => true })
// Spec says 200/201 → server should not return 404/500
expect(res.status).not.toBe(404)
expect(res.status).not.toBe(500)
}
}
}
})
Part 7: Structuring Multi-Product Docs (Like bKash)
bKash has multiple API products (Tokenized Checkout, Checkout URL, Auth & Capture, Disbursement). Their sidebar groups these clearly.
7.1 Multi-Spec Structure
api-docs/
├── specs/
│ ├── tokenized-checkout/
│ │ ├── openapi.yaml
│ │ └── examples/
│ ├── checkout-url/
│ │ ├── openapi.yaml
│ │ └── examples/
│ ├── auth-capture/
│ │ ├── openapi.yaml
│ │ └── examples/
│ └── disbursement/
│ ├── openapi.yaml
│ └── examples/
├── docs/
│ ├── index.html # Landing page
│ ├── tokenized-checkout/ # Built from specs/tokenized-checkout
│ ├── checkout-url/
│ ├── auth-capture/
│ └── disbursement/
└── scripts/
└── build-all.js
7.2 Landing Page
<!-- docs/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Merchant API Documentation</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', system-ui, sans-serif;
background: #F8FAFC;
color: #0F172A;
}
.container { max-width: 800px; margin: 80px auto; padding: 0 24px; }
h1 { font-size: 2rem; margin-bottom: 8px; }
p.subtitle { color: #64748B; margin-bottom: 40px; }
.card {
background: white;
border: 1px solid #E2E8F0;
border-radius: 12px;
padding: 24px;
margin-bottom: 16px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.card:hover {
border-color: #94A3B8;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.card h2 { font-size: 1.125rem; margin-bottom: 4px; }
.card p { color: #64748B; font-size: 0.875rem; }
a { text-decoration: none; color: inherit; display: block; }
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
background: #DBEAFE;
color: #1D4ED8;
margin-top: 8px;
}
</style>
</head>
<body>
<div class="container">
<h1>Merchant Payment API</h1>
<p class="subtitle">
Complete API reference for integrating payment services.
Select a product below to view its API specification.
</p>
<a href="/tokenized-checkout/">
<div class="card">
<h2>Tokenized Checkout</h2>
<p>Create agreements, process payments, and handle refunds with tokenized checkout — ideal for recurring payments.</p>
<span class="badge">v1.2.0-beta</span>
</div>
</a>
<a href="/checkout-url/">
<div class="card">
<h2>Checkout (URL Based)</h2>
<p>Generate checkout URLs for one-time payments. Customers complete payment on bKash's hosted page.</p>
<span class="badge">v1.1.0</span>
</div>
</a>
<a href="/auth-capture/">
<div class="card">
<h2>Auth & Capture</h2>
<p>Authorize a payment amount, then capture it later — useful for escrow and pre-authorization flows.</p>
<span class="badge">v1.0.0</span>
</div>
</a>
</div>
</body>
</html>
7.3 Build Script
// scripts/build-all.js
import { execSync } from 'child_process'
import { readdirSync, cpSync, mkdirSync } from 'fs'
const specDirs = readdirSync('specs/', { withFileTypes: true })
.filter(d => d.isDirectory())
for (const dir of specDirs) {
const name = dir.name
const specPath = `specs/${name}/openapi.yaml`
const outputDir = `docs/${name}`
mkdirSync(outputDir, { recursive: true })
execSync(
`npx @redocly/cli build-docs ${specPath} -o ${outputDir}/index.html`,
{ stdio: 'inherit' },
)
console.log(`✓ Built ${name} → ${outputDir}/index.html`)
}
Part 8: Design Best Practices (The bKash Way)
8.1 Parameter Table Design
bKash’s tables follow a consistent pattern: Property, Type, Description for responses; Property, Presence, Type, Description for request parameters. Your OpenAPI spec drives this:
parameters:
- name: authorization
in: header
required: true
description: Bearer token from Grant Token API
schema:
type: string
example: "Bearer eyJhbGciOi..."
Redoc renders this as:
| Property | Type | Required | Description |
|---|---|---|---|
authorization | string | ✅ Yes | Bearer token from Grant Token API |
8.2 Code Sample Tabs
Include multiple client examples in your spec descriptions or use Redoc’s x-codeSamples extension:
paths:
/payment/create:
post:
x-codeSamples:
- lang: cURL
source: |
curl -X POST https://sandbox-api.merchant.com/payment/create \
-H 'Authorization: Bearer {{token}}' \
-H 'Content-Type: application/json' \
-d '{"amount": "500", "currency": "BDT"}'
- lang: JavaScript
source: |
const response = await axios.post(
'https://sandbox-api.merchant.com/payment/create',
{ amount: '500', currency: 'BDT' },
{ headers: { Authorization: `Bearer ${token}` } }
)
- lang: Python
source: |
import requests
response = requests.post(
'https://sandbox-api.merchant.com/payment/create',
json={'amount': '500', 'currency': 'BDT'},
headers={'Authorization': f'Bearer {token}'}
)
8.3 Response Visualization
bKash shows sample request and sample response as formatted JSON code blocks. Redoc generates these automatically from the schema:
responses:
'200':
description: Payment initiated
content:
application/json:
example:
statusCode: '0000'
statusMessage: Successful
paymentID: TR0011dQPHnuY1720518383420
transactionStatus: Initiated
bkashURL: https://sandbox.bkash.com/pay?paymentID=TR0011...
8.4 Error Code Reference
A dedicated error codes page (like bKash’s) is essential. You can generate it from a custom OpenAPI extension or a standalone markdown file:
# In your spec, use x-errors for custom error code tables
x-error-codes:
- code: '2001'
message: Invalid App Key
description: The provided app_key does not match any registered application.
- code: '2023'
message: Insufficient Balance
description: The merchant account does not have sufficient balance.
- code: '2057'
message: Not a bKash Account
description: The provided wallet number is not a valid bKash account.
Then render this as a standalone page alongside your spec-driven reference.
Part 9: Comparison Summary
| Approach | Setup Effort | bKash-like Style | Try-it Console | Search | Bundle Size | Best For |
|---|---|---|---|---|---|---|
| Redoc single HTML | 🟢 5 min | ⭐⭐⭐⭐⭐ | ❌ | ⚠️ Plugin | ~2 MB | Pure reference sites |
| Swagger UI | 🟢 10 min | ⭐⭐⭐ | ✅ | ❌ | ~3 MB | Interactive sandboxes |
| Stoplight Elements + React | 🟡 30 min | ⭐⭐⭐⭐ | ✅ | ✅ | ~200 KB gzip | Full-featured docs |
| Scalar + Vue/Nuxt | 🟡 20 min | ⭐⭐⭐⭐⭐ | ✅ | ✅ | ~150 KB gzip | Modern docs sites |
| Docusaurus + Redoc | 🟠 1 hr | ⭐⭐⭐⭐⭐ | ❌ | ✅ | ~500 KB | Guides + reference |
| ReadMe (hosted) | 🟢 15 min | ⭐⭐⭐⭐⭐ | ✅ | ✅ | Hosted | Teams with budget (paid) |
Part 10: Best Practices
10.1 Spec Hygiene
- One spec file per API product — bKash has separate specs for Tokenized Checkout, Checkout URL, Auth & Capture, and Disbursement
- Reuse components —
$refshared schemas instead of copying them operationIdmust be unique — tools use it for URL routing- Every parameter needs a description — this is what shows up in the docs table
- Provide examples —
example/exampleson every property makes the reference useful - Use
enumfor constrained values — Redoc renders these as badges - Tag every operation — tags drive the sidebar navigation
10.2 Documentation UX
- Group by use case, not by HTTP method — bKash groups by product flow, not
GETvsPOST - Provide at least cURL + one language example per endpoint
- Add an environment selector — users shouldn’t edit URLs by hand
- Show schema examples inline — a collapsed JSON example is worth 1000 words of descriptions
- Include error codes — every API consumer needs this page
- Link between guides and reference — bKash’s sidebar mixes “Quick Start” guides with API specs
10.3 CI/CD
- Validate spec on every PR —
redocly lintcatches missing descriptions, invalid references, broken schemas - Auto-deploy on merge to main — documentation should ship as fast as code
- Version the spec alongside the code — the OpenAPI file lives in the same repo as the implementation
- Contract-test the spec against the implementation — ensure what’s documented actually matches the running API
Conclusion
Spec-driven documentation transforms API docs from a maintenance burden into a natural output of the development process. Write the OpenAPI spec once, validate it in CI, generate beautiful reference pages with any of the tools we’ve covered, and deploy automatically.
The bKash Developer Reference style — clean sidebar navigation, parameter tables, tabbed code samples, response payloads — is achievable with free, open-source tools. You don’t need ReadMe’s paid plan to match the quality; Redoc + a theming pass gets you 90% of the way there, and Stoplight Elements or Scalar can close the remaining gap.
What to do next:
- Start with a single well-structured OpenAPI spec for one API product
- Generate docs with
redocly build-docsand review the output - Add
x-codeSamplesfor real-world code examples - Set up CI validation:
redocly linton every PR - Auto-deploy to GitHub Pages / Netlify on merge to main
The best API documentation isn’t the one with the fanciest design. It’s the one that’s accurate, up-to-date, and costs nothing to maintain. Spec-driven docs are all three. 🚀
Further Reading
- OpenAPI Specification 3.0 — The spec that defines the spec
- Redocly CLI Documentation — Lint, bundle, build
- Redoc — Open-source reference docs engine
- Swagger UI — Interactive API explorer
- Stoplight Elements — React API reference component
- Scalar API Reference — Modern docs component
- Docusaurus + OpenAPI — Docs site with spec integration
- bKash Developer Reference — Design target & inspiration
- OpenAPI Specification on GitHub