API Documentation (OpenAPI)
Package: @onebun/docs
Overview
OneBun automatically generates OpenAPI 3.1 documentation from your controllers, decorators, and ArkType validation schemas. Install the package and get a Swagger UI with zero configuration.
Key features:
- Automatic OpenAPI 3.1 spec generation from route metadata
- ArkType schemas → JSON Schema conversion (one schema = type + validation + docs)
- Swagger UI served at
/docs - OpenAPI JSON spec served at
/openapi.json @ApiTags,@ApiOperation,@ApiResponsedecorators for additional metadata
Installation
bun add @onebun/docsThat's it. When @onebun/docs is installed, documentation is automatically enabled on application startup. No imports or configuration required in your application code.
How It Works
- On startup, the framework detects
@onebun/docsvia dynamic import - All controller metadata (routes, parameters, validation schemas) is collected
- An OpenAPI 3.1 specification is generated from this metadata
- Swagger UI is served at the configured path (default:
/docs)
Application starts
↓
Detects @onebun/docs installed
↓
Collects controller metadata (routes, @Body schemas, @Param, @Query, @Header)
↓
Converts ArkType schemas to JSON Schema
↓
Generates OpenAPI 3.1 spec
↓
Serves Swagger UI at /docs
Serves OpenAPI JSON at /openapi.jsonConfiguration
Customize documentation via the docs option in OneBunApplication:
import { OneBunApplication } from '@onebun/core';
import { AppModule } from './app.module';
const app = new OneBunApplication(AppModule, {
port: 3000,
docs: {
enabled: true, // default: true (if @onebun/docs installed)
path: '/docs', // Swagger UI path (default: '/docs')
jsonPath: '/openapi.json', // OpenAPI JSON path (default: '/openapi.json')
title: 'My API', // API title
version: '2.0.0', // API version
description: 'My awesome OneBun API',
contact: {
name: 'API Support',
email: 'support@example.com',
url: 'https://example.com',
},
license: {
name: 'MIT',
url: 'https://opensource.org/licenses/MIT',
},
servers: [
{ url: 'https://api.example.com', description: 'Production' },
{ url: 'http://localhost:3000', description: 'Development' },
],
},
});
await app.start();
// Swagger UI: http://localhost:3000/docs
// OpenAPI JSON: http://localhost:3000/openapi.jsonDocsApplicationOptions
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable/disable docs (auto-disabled if @onebun/docs not installed) |
path | string | '/docs' | Swagger UI path |
jsonPath | string | '/openapi.json' | OpenAPI JSON spec path |
title | string | App name or 'OneBun API' | API title in spec |
version | string | '1.0.0' | API version in spec |
description | string | - | API description |
contact | object | - | Contact info (name, email, url) |
license | object | - | License info (name, url) |
servers | array | - | Server URLs with descriptions |
externalDocs | object | - | External docs link (description, url) |
Disabling Documentation
// Explicitly disable
const app = new OneBunApplication(AppModule, {
docs: { enabled: false },
});
// Or simply don't install @onebun/docs — docs are silently skippedDocumentation Decorators
@ApiTags()
Group endpoints under tags in the Swagger UI. Imported from @onebun/docs.
import { Controller, BaseController, Get } from '@onebun/core';
import { ApiTags } from '@onebun/docs';
@ApiTags('Users', 'User Management')
@Controller('/users')
export class UserController extends BaseController {
// All endpoints in this controller are tagged with 'Users' and 'User Management'
@Get('/')
async findAll() {
return [];
}
}Can also be used on individual methods (place above the route decorator):
@ApiTags('Admin')
@Get('/admins')
async getAdmins() {
return [];
}@ApiOperation()
Describe an API operation with summary, description, and additional tags. Imported from @onebun/docs.
import { Controller, BaseController, Get, Post, Param, Body, type } from '@onebun/core';
import { ApiOperation } from '@onebun/docs';
@Controller('/users')
export class UserController extends BaseController {
@ApiOperation({
summary: 'Get user by ID',
description: 'Returns a single user by their unique identifier. Returns 404 if not found.',
tags: ['Users'],
})
@Get('/:id')
async getUser(@Param('id') id: string) {
return { id, name: 'John' };
}
}@ApiResponse()
Define response schemas for documentation and validation. Imported from @onebun/core.
import { Controller, BaseController, Get, Param, ApiResponse, type } from '@onebun/core';
const userSchema = type({
id: 'string',
name: 'string',
email: 'string.email',
});
@Controller('/users')
export class UserController extends BaseController {
@Get('/:id')
// @ApiResponse BELOW @Get
@ApiResponse(200, {
schema: userSchema,
description: 'User found successfully',
})
@ApiResponse(404, {
description: 'User not found',
})
async getUser(@Param('id') id: string) {
return { id, name: 'John', email: 'john@example.com' };
}
}ArkType to OpenAPI Schema
ArkType schemas passed to @Body(schema) or @ApiResponse(code, { schema }) are automatically converted to OpenAPI-compatible JSON Schema. This means one schema definition serves as TypeScript type, runtime validation, and OpenAPI documentation — no separate declarations to keep in sync.
import { type } from '@onebun/core';
// Define schema once (in a schema file)
const createUserSchema = type({
name: 'string',
email: 'string.email',
'age?': 'number > 0',
});
type CreateUserBody = typeof createUserSchema.infer;
// Use in @Body — generates both validation AND OpenAPI request body schema
@Post('/')
async createUser(@Body(createUserSchema) body: CreateUserBody) {
// body is validated and typed
return body;
}The resulting OpenAPI spec will include:
{
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"age": { "type": "number", "exclusiveMinimum": 0 }
},
"required": ["name", "email"]
}
}
}
}
}Programmatic Usage
For advanced use cases (CI pipelines, custom documentation tools):
import { generateOpenApiSpec, generateSwaggerUiHtml, arktypeToJsonSchema } from '@onebun/docs';
import { getControllerMetadata } from '@onebun/core';
// Generate OpenAPI spec from controller classes
const spec = generateOpenApiSpec(
[UserController, OrderController],
{
title: 'My API',
version: '1.0.0',
description: 'Generated API documentation',
},
);
// Convert spec to JSON
const json = JSON.stringify(spec, null, 2);
// Generate Swagger UI HTML pointing to a spec URL
const html = generateSwaggerUiHtml('/openapi.json');
// Convert an ArkType schema to JSON Schema
import { type } from '@onebun/core';
const schema = type({ name: 'string', age: 'number' });
const jsonSchema = arktypeToJsonSchema(schema);Complete Example
// src/config.ts
import { Env, type InferConfigType } from '@onebun/core';
export const envSchema = {
server: {
port: Env.number({ default: 3000, env: 'PORT' }),
},
};
export type AppConfig = InferConfigType<typeof envSchema>;
declare module '@onebun/core' {
interface OneBunAppConfig extends AppConfig {}
}
// src/user.controller.ts
import {
Controller,
BaseController,
Get,
Post,
Put,
Delete,
Param,
Body,
Query,
ApiResponse,
HttpException,
Service,
BaseService,
Module,
type,
} from '@onebun/core';
import { ApiTags, ApiOperation } from '@onebun/docs';
// ---- Schemas (in a real app, these live in a separate file e.g. user.schema.ts) ----
const userSchema = type({
id: 'string',
name: 'string',
email: 'string.email',
'age?': 'number > 0',
});
type User = typeof userSchema.infer;
const createUserSchema = type({
name: 'string',
email: 'string.email',
'age?': 'number > 0',
});
type CreateUserBody = typeof createUserSchema.infer;
const updateUserSchema = type({
'name?': 'string',
'email?': 'string.email',
'age?': 'number > 0',
});
type UpdateUserBody = typeof updateUserSchema.infer;
// ---- Service ----
@Service()
class UserService extends BaseService {
private users = new Map<string, User>();
async findAll(): Promise<User[]> {
return Array.from(this.users.values());
}
async findById(id: string): Promise<User | null> {
return this.users.get(id) || null;
}
async create(data: CreateUserBody): Promise<User> {
const user = { id: crypto.randomUUID(), ...data };
this.users.set(user.id, user);
return user;
}
async update(id: string, data: UpdateUserBody): Promise<User | null> {
const user = this.users.get(id);
if (!user) return null;
const updated = { ...user, ...data };
this.users.set(id, updated);
return updated;
}
async delete(id: string): Promise<boolean> {
return this.users.delete(id);
}
}
// ---- Controller with full documentation ----
@ApiTags('Users')
@Controller('/api/users')
class UserController extends BaseController {
constructor(private userService: UserService) {
super();
}
@ApiOperation({ summary: 'List all users', description: 'Returns all users. Supports pagination via query params.' })
@Get('/')
@ApiResponse(200, { schema: userSchema.array(), description: 'List of users' })
async findAll(
@Query('limit') limit?: string,
@Query('offset') offset?: string,
) {
return await this.userService.findAll();
}
@ApiOperation({ summary: 'Get user by ID' })
@Get('/:id')
@ApiResponse(200, { schema: userSchema, description: 'User found' })
@ApiResponse(404, { description: 'User not found' })
async findOne(@Param('id') id: string) {
const user = await this.userService.findById(id);
if (!user) throw new HttpException(404, 'User not found');
return user;
}
@ApiOperation({ summary: 'Create a new user' })
@Post('/')
@ApiResponse(201, { schema: userSchema, description: 'User created' })
@ApiResponse(400, { description: 'Invalid input' })
async create(@Body(createUserSchema) body: CreateUserBody) {
const user = await this.userService.create(body);
return this.success(user, 201);
}
@ApiOperation({ summary: 'Update a user' })
@Put('/:id')
@ApiResponse(200, { schema: userSchema, description: 'User updated' })
@ApiResponse(404, { description: 'User not found' })
async update(
@Param('id') id: string,
@Body(updateUserSchema) body: UpdateUserBody,
) {
const user = await this.userService.update(id, body);
if (!user) throw new HttpException(404, 'User not found');
return user;
}
@ApiOperation({ summary: 'Delete a user' })
@Delete('/:id')
@ApiResponse(200, { description: 'User deleted' })
@ApiResponse(404, { description: 'User not found' })
async delete(@Param('id') id: string) {
const deleted = await this.userService.delete(id);
if (!deleted) throw new HttpException(404, 'User not found');
return { deleted: true };
}
}
// ---- Module ----
@Module({
controllers: [UserController],
providers: [UserService],
})
class UserModule {}
// ---- Application ----
import { OneBunApplication } from '@onebun/core';
const app = new OneBunApplication(UserModule, {
port: 3000,
docs: {
title: 'User Management API',
version: '1.0.0',
description: 'API for managing users',
},
});
await app.start();
// Swagger UI: http://localhost:3000/docs
// OpenAPI JSON: http://localhost:3000/openapi.json
// API endpoint: http://localhost:3000/api/usersAfter starting the application:
- Visit
http://localhost:3000/docsfor interactive Swagger UI - Fetch
http://localhost:3000/openapi.jsonfor the raw OpenAPI specification - All endpoints, request/response schemas, and parameter types are auto-documented
