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>)
Configuration options for the API instanceURL prefix for all routes (e.g., /api/v1)
OpenAPI specification configurationAPI version (e.g., "1.0.0")
options.openapi.description
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
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
Route configurationRoute path with optional parameters (e.g., /users/:id)
HTTP method: "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"
Request validation schema Response validation schema (maps status codes to schemas)
Route-specific middlewares
config.handler
RouteHandler<TReq, TRes, TExt>
required
Route handler function
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>
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
Callback function called when server starts
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 configurationRequest validation schema for middleware
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
Underlying Web API Request object
Validated request datareq.body
InferOutput<TReq['body']>
Validated request body
req.params
InferOutput<TReq['params']>
Validated path parameters
req.query
InferOutput<TReq['query']>
Validated query parameters
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();