Skip to content

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, @ApiResponse decorators for additional metadata

Installation

bash
bun add @onebun/docs

That'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

  1. On startup, the framework detects @onebun/docs via dynamic import
  2. All controller metadata (routes, parameters, validation schemas) is collected
  3. An OpenAPI 3.1 specification is generated from this metadata
  4. 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.json

Configuration

Customize documentation via the docs option in OneBunApplication:

typescript
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.json

DocsApplicationOptions

OptionTypeDefaultDescription
enabledbooleantrueEnable/disable docs (auto-disabled if @onebun/docs not installed)
pathstring'/docs'Swagger UI path
jsonPathstring'/openapi.json'OpenAPI JSON spec path
titlestringApp name or 'OneBun API'API title in spec
versionstring'1.0.0'API version in spec
descriptionstring-API description
contactobject-Contact info (name, email, url)
licenseobject-License info (name, url)
serversarray-Server URLs with descriptions
externalDocsobject-External docs link (description, url)

Disabling Documentation

typescript
// Explicitly disable
const app = new OneBunApplication(AppModule, {
  docs: { enabled: false },
});

// Or simply don't install @onebun/docs — docs are silently skipped

Documentation Decorators

@ApiTags()

Group endpoints under tags in the Swagger UI. Imported from @onebun/docs.

typescript
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):

typescript
@ApiTags('Admin')
@Get('/admins')
async getAdmins() {
  return [];
}

@ApiOperation()

Describe an API operation with summary, description, and additional tags. Imported from @onebun/docs.

typescript
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.

typescript
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.

typescript
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:

json
{
  "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):

typescript
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

typescript
// 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/users

After starting the application:

  • Visit http://localhost:3000/docs for interactive Swagger UI
  • Fetch http://localhost:3000/openapi.json for the raw OpenAPI specification
  • All endpoints, request/response schemas, and parameter types are auto-documented

Released under the MPL-2.0 License.