Skip to main content

API Framework

A lightweight, type-safe REST API framework built on Bun’s native routing with automatic validation and OpenAPI spec generation.

Requirements

Bun Runtime Required: This API framework is built specifically for the Bun runtime and uses Bun-native APIs including Bun.serve(), Bun.CookieMap, and optimized routing.
curl -fsSL https://bun.sh/install | bash

Import

import { Api, Middleware } from "semola/api";

Classes

Api

The main API class for creating and managing routes.

Constructor

new Api<TMiddlewares extends readonly Middleware[] = readonly []>(options?: ApiOptions<TMiddlewares>)
options
ApiOptions<TMiddlewares>
Configuration options for the API instance
options.prefix
string
URL prefix for all routes (e.g., /api/v1)
options.openapi
OpenApiOptions
OpenAPI specification configuration
options.openapi.title
string
required
API title
options.openapi.version
string
required
API version (e.g., "1.0.0")
options.openapi.description
string
API description
options.openapi.servers
Array<{ url: string; description?: string }>
Server configurations
options.openapi.securitySchemes
Record<string, SecurityScheme>
Security scheme definitions
options.validation
ValidationOptions
default:"true"
Enable/disable input and output validationCan be:
  • true - Enable all validation (default)
  • false - Disable all validation
  • { input?: boolean; output?: boolean } - Granular control
options.middlewares
TMiddlewares
Global middlewares to apply to all routes
Example:
const api = new Api({
  prefix: "/api/v1",
  openapi: {
    title: "My API",
    description: "A type-safe REST API",
    version: "1.0.0",
  },
  validation: {
    input: true,
    output: true,
  },
});

Methods

defineRoute
Defines a route with type-safe request/response validation.
defineRoute<
  TReq extends RequestSchema = RequestSchema,
  TRes extends ResponseSchema | undefined = undefined,
  TRouteMiddlewares extends readonly Middleware[] = readonly []
>(config: RouteConfig<TReq, TRes, TGlobalMiddlewares, TRouteMiddlewares>): void
config
RouteConfig
required
Route configuration
config.path
string
required
Route path with optional parameters (e.g., /users/:id)
config.method
HTTPMethod
required
HTTP method: "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"
config.request
RequestSchema
Request validation schema
config.request.params
StandardSchemaV1
Path parameters schema
config.request.body
StandardSchemaV1
Request body schema
config.request.query
StandardSchemaV1
Query parameters schema
config.request.headers
StandardSchemaV1
Headers schema
config.request.cookies
StandardSchemaV1
Cookies schema
config.response
ResponseSchema
Response validation schema (maps status codes to schemas)
config.middlewares
TRouteMiddlewares
Route-specific middlewares
config.handler
RouteHandler<TReq, TRes, TExt>
required
Route handler function
config.summary
string
OpenAPI summary
config.description
string
OpenAPI description
config.operationId
string
OpenAPI operation ID
config.tags
string[]
OpenAPI tags
Example:
import { z } from "zod";

api.defineRoute({
  path: "/users/:id",
  method: "GET",
  summary: "Get user by ID",
  operationId: "getUserById",
  tags: ["Users"],
  request: {
    params: z.object({
      id: z.string().uuid(),
    }),
  },
  response: {
    200: z.object({
      id: z.string(),
      name: z.string(),
      email: z.string().email(),
    }),
    404: z.object({
      message: z.string(),
    }),
  },
  handler: async (c) => {
    const user = await getUser(c.req.params.id);
    
    if (!user) {
      return c.json(404, { message: "User not found" });
    }
    
    return c.json(200, user);
  },
});
getOpenApiSpec
Generates an OpenAPI 3.1.0 specification from defined routes.
async getOpenApiSpec(): Promise<OpenAPISpec>
spec
object
OpenAPI 3.1.0 specification object
Example:
const spec = await api.getOpenApiSpec();
console.log(JSON.stringify(spec, null, 2));
serve
Starts the server on the specified port.
serve(port: number, callback?: () => void): Bun.Server
port
number
required
Port number to listen on
callback
() => void
Callback function called when server starts
server
Bun.Server
Bun server instance
Example:
api.serve(3000, () => {
  console.log("Server running on http://localhost:3000");
});

Middleware

Defines middleware that runs before route handlers.

Constructor

new Middleware<
  TReq extends RequestSchema = RequestSchema,
  TRes extends ResponseSchema | undefined = undefined,
  TExt extends Record<string, unknown> = Record<string, unknown>
>(options: MiddlewareOptions<TReq, TRes, TExt>)
options
MiddlewareOptions<TReq, TRes, TExt>
required
Middleware configuration
options.request
RequestSchema
Request validation schema for middleware
options.response
ResponseSchema
Response schema for early returns
options.handler
MiddlewareHandler<TReq, TRes, TExt>
required
Middleware handler function that can return context data or a Response
Example:
import { Middleware } from "semola/api";
import { z } from "zod";

const authMiddleware = new Middleware({
  request: {
    headers: z.object({
      authorization: z.string(),
    }),
  },
  response: {
    401: z.object({ error: z.string() }),
  },
  handler: async (c) => {
    const token = c.req.headers.authorization;
    
    if (!token || !token.startsWith("Bearer ")) {
      return c.json(401, { error: "Unauthorized" });
    }
    
    const user = await validateToken(token.slice(7));
    
    if (!user) {
      return c.json(401, { error: "Invalid token" });
    }
    
    // Return data to extend context
    return { user };
  },
});

Context Object

The handler context object passed to route handlers and middlewares.

Properties

raw
Request
Underlying Web API Request object
req
object
Validated request data
req.body
InferOutput<TReq['body']>
Validated request body
req.params
InferOutput<TReq['params']>
Validated path parameters
req.query
InferOutput<TReq['query']>
Validated query parameters
req.headers
InferOutput<TReq['headers']>
Validated headers
req.cookies
InferOutput<TReq['cookies']>
Validated cookies

Methods

json
<S extends number>(status: S, data: unknown) => Response
Returns a JSON response with validation (if output validation enabled)
text
(status: number, text: string) => Response
Returns a plain text response
html
(status: number, html: string) => Response
Returns an HTML response
redirect
(status: number, url: string) => Response
Returns an HTTP redirect response
get
<K extends keyof TExt>(key: K) => TExt[K]
Gets data from middleware context

Type Definitions

RequestSchema

type RequestSchema = {
  params?: StandardSchemaV1;
  body?: StandardSchemaV1;
  query?: StandardSchemaV1;
  headers?: StandardSchemaV1;
  cookies?: StandardSchemaV1;
};

ResponseSchema

type ResponseSchema = {
  [status: number]: StandardSchemaV1;
};

ValidationOptions

type ValidationOptions =
  | boolean
  | {
      input?: boolean;
      output?: boolean;
    };

SecurityScheme

type SecuritySchemeApiKey = {
  type: "apiKey";
  name: string;
  in: "query" | "header" | "cookie";
  description?: string;
};

type SecuritySchemeHttp = {
  type: "http";
  scheme: string;
  bearerFormat?: string;
  description?: string;
};

type SecuritySchemeOAuth2 = {
  type: "oauth2";
  flows: {
    implicit?: SecuritySchemeOAuth2Flow;
    password?: SecuritySchemeOAuth2Flow;
    clientCredentials?: SecuritySchemeOAuth2Flow;
    authorizationCode?: SecuritySchemeOAuth2Flow;
  };
  description?: string;
};

type SecuritySchemeOpenIdConnect = {
  type: "openIdConnect";
  openIdConnectUrl: string;
  description?: string;
};

type SecurityScheme =
  | SecuritySchemeApiKey
  | SecuritySchemeHttp
  | SecuritySchemeOAuth2
  | SecuritySchemeOpenIdConnect;

Usage Examples

Basic API with Validation

import { z } from "zod";
import { Api } from "semola/api";

const api = new Api({
  prefix: "/api/v1",
  openapi: {
    title: "User API",
    version: "1.0.0",
  },
});

api.defineRoute({
  path: "/users",
  method: "POST",
  request: {
    body: z.object({
      name: z.string().min(1),
      email: z.string().email(),
    }),
  },
  response: {
    201: z.object({
      id: z.string().uuid(),
      name: z.string(),
      email: z.string().email(),
    }),
  },
  handler: async (c) => {
    const user = await createUser(c.req.body);
    return c.json(201, user);
  },
});

api.serve(3000);

With Global Middleware

import { Api, Middleware } from "semola/api";

const loggingMiddleware = new Middleware({
  handler: async (c) => {
    const start = Date.now();
    console.log(`${c.raw.method} ${c.raw.url}`);
    return { requestStartTime: start };
  },
});

const api = new Api({
  middlewares: [loggingMiddleware] as const,
});

api.defineRoute({
  path: "/users",
  method: "GET",
  handler: async (c) => {
    const startTime = c.get("requestStartTime");
    const users = await getUsers();
    console.log(`Request took ${Date.now() - startTime}ms`);
    return c.json(200, users);
  },
});

Schema Reuse with OpenAPI

import { z } from "zod";

// Define reusable schemas with IDs
const UserSchema = z
  .object({
    id: z.string().uuid(),
    name: z.string(),
    email: z.string().email(),
  })
  .meta({ id: "User" });

const ErrorSchema = z
  .object({
    message: z.string(),
  })
  .meta({ id: "ErrorResponse" });

// Use across routes
api.defineRoute({
  path: "/users/:id",
  method: "GET",
  response: { 200: UserSchema, 404: ErrorSchema },
  handler: async (c) => {
    // ...
  },
});

// UserSchema and ErrorSchema defined once in components.schemas
const spec = await api.getOpenApiSpec();

Build docs developers (and LLMs) love