Hono + OpenAPI Integration
TinyKit Pro uses Hono with @hono/zod-openapi for HTTP endpoints, providing automatic OpenAPI specification generation, interactive API documentation, and typ...
TinyKit Pro uses Hono with @hono/zod-openapi for HTTP endpoints, providing automatic OpenAPI specification generation, interactive API documentation, and type-safe routing.
Overview
Technology Stack
- Hono: Fast, lightweight web framework for HTTP endpoints
- @hono/zod-openapi: OpenAPI integration with Zod schemas
- @scalar/hono-api-reference: Interactive API documentation UI
- Zod: Runtime type validation (consistent with rest of TinyKit)
Key Benefits
✅ Automatic OpenAPI Spec - Generated from Zod schemas
✅ Component-Based Specs - 15-24x smaller than inline schemas via $ref reuse
✅ Type-Safe Routing - Full TypeScript autocomplete for routes
✅ Interactive API Docs - Scalar UI at /scalar endpoint
✅ Zod Consistency - Same schema library across entire project
✅ Better DX - Cleaner route definitions with validation
Architecture
File Structure
convex/
├── http.ts # Main Hono router with OpenAPI
├── billing/
│ └── api.ts # Billing routes (OpenAPI)
├── emails/
│ └── api.ts # Email webhook routes (OpenAPI)
└── siteSettings/
└── api.ts # Site settings routes (OpenAPI)Main Router (convex/http.ts)
The main HTTP router uses OpenAPIHono wrapped with HttpRouterWithHono for Convex compatibility:
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import { HttpRouterWithHono } from "convex-helpers/server/hono";
import { cors } from "hono/cors";
import { Scalar } from "@scalar/hono-api-reference";
import { ActionCtx } from "./_generated/server";
import { auth } from "./auth";
/**
* Convex environment type for Hono context
*/
type ConvexEnv = {
Bindings: ActionCtx;
};
/**
* Create Hono app with OpenAPI support
*/
const app = new OpenAPIHono<ConvexEnv>();
/**
* Enable CORS for all routes
*/
app.use("/*", cors());
/**
* Register domain-specific route modules
*/
siteSettingsRoutes(app);
emailRoutes(app);
billingRoutes(app);
/**
* Scalar API documentation UI
*/
app.get("/scalar", Scalar({ url: "/openapi" }));
/**
* OpenAPI documentation endpoint
*/
app.doc("/openapi", {
openapi: "3.0.0",
info: {
version: "1.0.0",
title: "TinyKit Pro API",
description: "REST API for TinyKit Pro SaaS boilerplate",
},
tags: [
{ name: "System", description: "Health and system endpoints" },
{ name: "Site Settings", description: "Site configuration" },
{ name: "Webhooks", description: "External service webhooks" },
],
});
/**
* Wrap with HttpRouterWithHono for Convex
*/
const http = new HttpRouterWithHono(app);
/**
* Register Convex Auth routes (OAuth, JWT)
*/
auth.addHttpRoutes(http);
export default http;Route Module Pattern
Creating OpenAPI Routes
Each route module follows a consistent pattern:
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import { ActionCtx } from "../_generated/server";
type ConvexEnv = {
Bindings: ActionCtx;
};
// Define Zod schemas
const RequestSchema = z
.object({
name: z.string().min(1),
email: z.string().email(),
})
.openapi("CreateUserRequest");
const ResponseSchema = z
.object({
id: z.string(),
name: z.string(),
email: z.string(),
})
.openapi("User");
const ErrorSchema = z
.object({
error: z.string(),
})
.openapi("Error");
// Define route configuration
const createUserRoute = createRoute({
method: "post",
path: "/users",
request: {
body: {
content: {
"application/json": {
schema: RequestSchema,
},
},
},
},
responses: {
201: {
content: {
"application/json": {
schema: ResponseSchema,
},
},
description: "User created successfully",
},
400: {
content: {
"application/json": {
schema: ErrorSchema,
},
},
description: "Validation error",
},
},
tags: ["Users"],
});
// Export route registration function
export function userRoutes(app: OpenAPIHono<ConvexEnv>) {
app.openapi(createUserRoute, async (c) => {
const body = c.req.valid("json");
// Access Convex context
const result = await c.env.runMutation(internal.users.create, body);
return c.json(result as never, 201);
});
}Available Endpoints
Documentation Endpoints
| Path | Method | Purpose |
|---|---|---|
/openapi | GET | OpenAPI 3.0 specification (JSON) |
/scalar | GET | Interactive API documentation UI |
System Endpoints
| Path | Method | Purpose | Module |
|---|---|---|---|
/health | GET | Health check for monitoring | Main router |
Site Settings
| Path | Method | Purpose | Module |
|---|---|---|---|
/og-settings | GET | OG image generation settings | siteSettings |
/og-image-url | GET | Custom OG image URL | siteSettings |
Webhooks
| Path | Method | Purpose | Module |
|---|---|---|---|
/stripe-webhook | POST | Stripe billing events | billing |
/resend-webhook | POST | Email delivery events | emails |
Authentication
Authentication endpoints are automatically registered by Convex Auth:
/api/auth/*- OAuth flows, sign-in, sign-up/.well-known/openid-configuration- OpenID Connect discovery/.well-known/jwks.json- JSON Web Key Set
Schema Patterns
Request/Response Schemas
Always define schemas with .openapi() to register them as components:
// Request schema
const CreateItemSchema = z
.object({
name: z.string().min(1).openapi({ example: "New Item" }),
description: z.string().optional(),
tags: z.array(z.string()).max(10),
})
.openapi("CreateItem");
// Response schema
const ItemSchema = z
.object({
_id: z.string(),
name: z.string(),
description: z.string().optional(),
tags: z.array(z.string()),
createdAt: z.number(),
})
.openapi("Item");
// Error schema (reusable)
const ErrorSchema = z
.object({
error: z.string(),
})
.openapi("Error");Route Path Parameters
Define path parameters in the schema:
const ParamsSchema = z.object({
id: z.string().openapi({
param: { name: "id", in: "path" },
example: "j97d2e3f4g5h6i7j8k9l0m1n",
}),
});
const getItemRoute = createRoute({
method: "get",
path: "/items/{id}",
request: { params: ParamsSchema },
responses: {
200: {
content: {
"application/json": {
schema: ItemSchema,
},
},
description: "Item retrieved successfully",
},
404: {
content: {
"application/json": {
schema: ErrorSchema,
},
},
description: "Item not found",
},
},
});
// Implementation
app.openapi(getItemRoute, async (c) => {
const { id } = c.req.valid("param");
// Use id...
});Response Patterns
JSON Responses
// Success response
return c.json({ id: "123", name: "Item" } as never, 200);
// Error response
return c.json({ error: "Not found" }, 404);
// With custom headers
return c.json(data as never, 200, {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600",
});Text Responses
// Plain text
return c.text("OK", 200);
// Error text
return c.text("Health Check Error", 500);Accessing Request Data
app.openapi(myRoute, async (c) => {
// Validated request body
const body = c.req.valid("json");
// Validated path parameters
const params = c.req.valid("param");
// Headers
const signature = c.req.header("stripe-signature");
// Raw request body (for webhooks)
const payload = await c.req.text();
// Convex context
const result = await c.env.runQuery(api.example.getData, {});
return c.json(result as never);
});OpenAPI Specification
Component-Based Architecture
@hono/zod-openapi generates component-based OpenAPI specs:
{
"openapi": "3.0.0",
"components": {
"schemas": {
"CreateItem": {
"type": "object",
"properties": {
"name": { "type": "string" }
}
},
"Item": {
"type": "object",
"properties": {
"_id": { "type": "string" },
"name": { "type": "string" }
}
}
}
},
"paths": {
"/items": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/CreateItem" }
}
}
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/Item" }
}
}
}
}
}
}
}
}Scalability Benefits
For a 50-route API:
- Component-based: ~10KB (efficient reuse)
- Inline approach: ~150KB+ (duplication)
- Impact: 15x smaller spec
For a 200-route API:
- Component-based: ~25KB (efficient at scale)
- Inline approach: ~600KB+ (exponential bloat)
- Impact: 24x smaller spec
API Documentation UI
TinyKit Pro provides two ways to access API documentation:
1. Convex Hono Integration (Built-in)
Access Scalar directly from your Convex deployment:
https://your-deployment.convex.site/scalarFeatures:
- ✅ Served directly from Convex (no Next.js required)
- ✅ Automatically fetches from
/openapiendpoint - ✅ Perfect for API-only deployments
- ✅ Can be accessed from any environment
2. Next.js App Router Integration
Access Scalar from your Next.js frontend:
http://localhost:3000/api/docs # Development
https://your-project.vercel.app/api/docs # ProductionFeatures:
- ✅ Integrated with Next.js app
- ✅ Fetches from Convex deployment via CORS
- ✅ Can be protected with Next.js auth
- ✅ Customizable with Next.js layouts
Comparison
| Feature | Convex (/scalar) | Next.js (/api/docs) |
|---|---|---|
| Package | @scalar/hono-api-reference | @scalar/nextjs-api-reference |
| Location | Convex deployment | Next.js frontend |
| CORS | Not needed | Required |
| Auth | Hono middleware | Next.js middleware |
| Best For | Public APIs, quick testing | Private docs, team dashboards |
Recommendation: Use both! Convex /scalar for quick testing and API consumers, Next.js /api/docs for internal team documentation.
Example Implementation
Site Settings Routes
Real example from TinyKit Pro:
// convex/siteSettings/api.ts
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import { ActionCtx } from "../_generated/server";
import { api } from "../_generated/api";
type ConvexEnv = { Bindings: ActionCtx };
const OGSettingsSchema = z
.object({
siteName: z.string(),
slogan: z.string().optional(),
description: z.string().optional(),
primaryColor: z.string().optional(),
secondaryColor: z.string().optional(),
})
.openapi("OGSettings");
const getOGSettingsRoute = createRoute({
method: "get",
path: "/og-settings",
responses: {
200: {
content: {
"application/json": {
schema: OGSettingsSchema,
},
},
description: "OG image settings retrieved successfully",
},
},
tags: ["Site Settings"],
});
export function siteSettingsRoutes(app: OpenAPIHono<ConvexEnv>) {
app.openapi(getOGSettingsRoute, async (c) => {
const settings = await c.env.runQuery(
api.siteSettings.public.queries.getPublicSettings,
);
const responseData = {
siteName: settings.siteName,
slogan: settings.slogan,
description: settings.description,
primaryColor: settings.ogImageSettings?.primaryColor,
secondaryColor: settings.ogImageSettings?.secondaryColor,
};
return c.json(responseData as never, 200, {
"Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*",
});
});
}Testing the API
Using curl
# Test endpoints
curl https://your-project.convex.site/health
curl https://your-project.convex.site/og-settings
# View OpenAPI spec
curl https://your-project.convex.site/openapiUsing the Scalar UI
- Navigate to
https://your-deployment.convex.site/scalar - Browse available endpoints
- Test endpoints interactively
- View request/response schemas
- Generate code snippets
Best Practices
Schema Design
✅ Always use .openapi("Name") to register schemas as components
✅ Provide examples for better docs: .openapi({ example: "value" })
✅ Reuse schemas across multiple routes
✅ Define error schemas once and reuse
Route Definition
✅ Separate route config from implementation for clarity
✅ Use descriptive route names (e.g., createAttributeRoute)
✅ Document all responses including error cases
✅ Include proper status codes (201 for create, 200 for update)
Type Safety
✅ Use c.req.valid("json") for validated request bodies
✅ Use c.req.valid("param") for validated path parameters
✅ Cast to never for Convex query results: c.json(result as never)
✅ Define ConvexEnv type for proper context typing
Migration Guide
From Standard httpRouter
If you're migrating from standard Convex httpRouter:
Before (httpRouter):
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
const http = httpRouter();
http.route({
path: "/items",
method: "GET",
handler: httpAction(async (ctx) => {
const items = await ctx.runQuery(api.items.list, {});
return new Response(JSON.stringify(items), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}),
});After (Hono + OpenAPI):
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import { HttpRouterWithHono } from "convex-helpers/server/hono";
const ItemSchema = z
.object({
_id: z.string(),
name: z.string(),
})
.openapi("Item");
const listItemsRoute = createRoute({
method: "get",
path: "/items",
responses: {
200: {
content: {
"application/json": {
schema: z.array(ItemSchema),
},
},
description: "List of items",
},
},
tags: ["Items"],
});
const app = new OpenAPIHono<ConvexEnv>();
app.openapi(listItemsRoute, async (c) => {
const items = await c.env.runQuery(api.items.list, {});
return c.json(items as never);
});
const http = new HttpRouterWithHono(app);
auth.addHttpRoutes(http); // Auth routes still work!
export default http;Key Differences
| Feature | httpRouter | Hono + OpenAPI |
|---|---|---|
| Response | new Response() | c.json(), c.text() |
| Request body | await request.json() | c.req.valid("json") |
| Headers | request.headers.get() | c.req.header() |
| Context | ctx | c.env |
| Documentation | Manual | Auto-generated |
| Validation | Manual | Automatic via Zod |
Troubleshooting
Common Issues
Issue: Type 'OpenAPIHono' is not assignable to 'HttpRouter'
Solution: Make sure route functions accept OpenAPIHono<ConvexEnv> not HttpRouter:
// ✅ Correct
export function myRoutes(app: OpenAPIHono<ConvexEnv>) {}
// ❌ Wrong
export function myRoutes(http: HttpRouter) {}Issue: TypeScript errors on c.json(result)
Solution: Cast Convex query results to never:
return c.json(result as never);Issue: Auth routes not working after migration
Solution: Ensure auth.addHttpRoutes(http) is called after wrapping with HttpRouterWithHono:
const app = new OpenAPIHono<ConvexEnv>();
// ... register routes
const http = new HttpRouterWithHono(app);
auth.addHttpRoutes(http); // Must be after wrapping
export default http;Resources
- Hono Documentation: https://hono.dev
- @hono/zod-openapi: https://hono.dev/examples/zod-openapi
- Scalar Documentation: https://scalar.com/docs
- OpenAPI 3.0 Spec: https://spec.openapis.org/oas/v3.0.0
- Zod Documentation: https://zod.dev
MCP Integration (llms.txt)
TinyKit Pro includes a Model Context Protocol (MCP) integration via the /llms.txt endpoint. This endpoint provides site information optimized for consumption...
R2 Artifact Storage
Documentation for Cloudflare R2 artifact storage used by the TinyKit CLI for distributing product tarballs.