TinyKit Pro Docs

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

PathMethodPurpose
/openapiGETOpenAPI 3.0 specification (JSON)
/scalarGETInteractive API documentation UI

System Endpoints

PathMethodPurposeModule
/healthGETHealth check for monitoringMain router

Site Settings

PathMethodPurposeModule
/og-settingsGETOG image generation settingssiteSettings
/og-image-urlGETCustom OG image URLsiteSettings

Webhooks

PathMethodPurposeModule
/stripe-webhookPOSTStripe billing eventsbilling
/resend-webhookPOSTEmail delivery eventsemails

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/scalar

Features:

  • ✅ Served directly from Convex (no Next.js required)
  • ✅ Automatically fetches from /openapi endpoint
  • ✅ 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  # Production

Features:

  • ✅ Integrated with Next.js app
  • ✅ Fetches from Convex deployment via CORS
  • ✅ Can be protected with Next.js auth
  • ✅ Customizable with Next.js layouts

Comparison

FeatureConvex (/scalar)Next.js (/api/docs)
Package@scalar/hono-api-reference@scalar/nextjs-api-reference
LocationConvex deploymentNext.js frontend
CORSNot neededRequired
AuthHono middlewareNext.js middleware
Best ForPublic APIs, quick testingPrivate 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/openapi

Using the Scalar UI

  1. Navigate to https://your-deployment.convex.site/scalar
  2. Browse available endpoints
  3. Test endpoints interactively
  4. View request/response schemas
  5. 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

FeaturehttpRouterHono + OpenAPI
Responsenew Response()c.json(), c.text()
Request bodyawait request.json()c.req.valid("json")
Headersrequest.headers.get()c.req.header()
Contextctxc.env
DocumentationManualAuto-generated
ValidationManualAutomatic 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


← Back to Technical Documentation

On this page

Ship your startup faster. In minutes.

Get TinyKit Pro