TinyKit Pro Docs

HTTP Routes Architecture

TinyKit Pro uses Hono with OpenAPI for HTTP endpoints, providing automatic API documentation, type-safe routing, and a modular domain-based organization patt...

TinyKit Pro uses Hono with OpenAPI for HTTP endpoints, providing automatic API documentation, type-safe routing, and a modular domain-based organization pattern.

Architecture Overview

HTTP routes are built with:

  • Hono: Fast, lightweight web framework
  • @hono/zod-openapi: Automatic OpenAPI spec generation
  • Modular Organization: Domain-specific route modules
  • Interactive Docs: Scalar UI at /scalar

For detailed Hono + OpenAPI documentation, see Hono + OpenAPI Integration.

Example File Structure

convex/
├── http.ts                    # Main router registration
├── billing/
│   └── api.ts                 # Billing/Stripe routes
├── emails/
│   └── api.ts                 # Email/Resend routes
└── siteSettings/
    └── api.ts                 # Site settings routes

Main Router (http.ts)

The main HTTP router uses Hono with OpenAPI and wraps it 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";
import { siteSettingsRoutes } from "./siteSettings/api";
import { emailRoutes } from "./emails/api";
import { billingRoutes } from "./billing/api";

type ConvexEnv = { Bindings: ActionCtx };

const app = new OpenAPIHono<ConvexEnv>();

// Enable CORS globally
app.use("/*", cors());

// Register domain-specific routes
siteSettingsRoutes(app);
emailRoutes(app);
billingRoutes(app);

// API documentation endpoints
app.get("/scalar", Scalar({ url: "/openapi" }));
app.doc("/openapi", {
  openapi: "3.0.0",
  info: {
    version: "1.0.0",
    title: "TinyKit Pro API",
    description: "REST API for TinyKit Pro SaaS boilerplate",
  },
});

// Wrap with HttpRouterWithHono for Convex
const http = new HttpRouterWithHono(app);

// Register Convex Auth routes
auth.addHttpRoutes(http);

export default http;

Route Modules

Module Pattern

Each route module follows the Hono + OpenAPI pattern:

import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import { ActionCtx } from "../_generated/server";

type ConvexEnv = { Bindings: ActionCtx };

// Define Zod schemas
const ResponseSchema = z
  .object({
    message: z.string(),
  })
  .openapi("Response");

// Define route configuration
const myRoute = createRoute({
  method: "get",
  path: "/endpoint-path",
  responses: {
    200: {
      content: {
        "application/json": {
          schema: ResponseSchema,
        },
      },
      description: "Success response",
    },
  },
  tags: ["Domain"],
});

/**
 * Register [domain] HTTP routes
 * @param app - OpenAPIHono app instance
 */
export function domainRoutes(app: OpenAPIHono<ConvexEnv>) {
  app.openapi(myRoute, async (c) => {
    // Access Convex context
    const result = await c.env.runQuery(api.example.getData, {});
    return c.json(result as never);
  });
}

Billing Routes (convex/billing/api.ts)

Handles Stripe webhook events:

export function billingRoutes(http: HttpRouter) {
  /**
   * Stripe webhook endpoint for subscription and payment events
   * Validates signature and delegates to handleStripeEvent
   */
  http.route({
    path: "/stripe-webhook",
    method: "POST",
    handler: httpAction(async (ctx, request) => {
      const signature = request.headers.get("stripe-signature")!;
      const payload = await request.text();

      const result = await ctx.runAction(
        internal.billing.internal.webhooks.handleStripeEvent,
        { signature, payload },
      );

      return result.success
        ? new Response(null, { status: 200 })
        : new Response("Webhook Error", { status: 400 });
    }),
  });
}

Email Routes (convex/emails/api.ts)

Handles Resend email webhooks:

export function emailRoutes(http: HttpRouter) {
  /**
   * Resend webhook endpoint for email event tracking
   * Handles delivery, bounce, and complaint notifications
   */
  http.route({
    path: "/resend-webhook",
    method: "POST",
    handler: httpAction(async (ctx, request) => {
      logger.info("🔔 Received Resend webhook request");

      const result = await resend.handleResendEventWebhook(ctx, request);

      logger.info("✅ Resend webhook processed successfully");
      return result;
    }),
  });
}

Site Settings Routes (convex/siteSettings/api.ts)

Public endpoints for OG image generation:

export function siteSettingsRoutes(http: HttpRouter) {
  /**
   * Public endpoint for OG image generation
   * Returns site settings for dynamic Open Graph images
   */
  http.route({
    path: "/og-settings",
    method: "GET",
    handler: httpAction(async (ctx) => {
      const settings = await ctx.runQuery(
        api.siteSettings.public.queries.getPublicSettings,
      );

      return new Response(JSON.stringify(settings.ogImageSettings), {
        status: 200,
        headers: {
          "Content-Type": "application/json",
          "Cache-Control": "public, max-age=3600",
          "Access-Control-Allow-Origin": "*",
        },
      });
    }),
  });

  /**
   * Custom OG image URL endpoint
   * Returns storage URL if custom image exists
   */
  http.route({
    path: "/og-image-url",
    method: "GET",
    handler: httpAction(async (ctx) => {
      const imageUrl = await ctx.runQuery(
        api.siteSettings.public.queries.getOGImageUrl,
      );

      return new Response(JSON.stringify({ imageUrl }), {
        status: 200,
        headers: {
          "Content-Type": "application/json",
          "Cache-Control": "public, max-age=3600",
          "Access-Control-Allow-Origin": "*",
        },
      });
    }),
  });
}

Available Endpoints

Public Endpoints

PathMethodPurposeModule
/healthGETHealth check for monitoringMain router
/og-settingsGETOG image generation settingssiteSettings
/og-image-urlGETCustom OG image URLsiteSettings

Webhook Endpoints

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

Authentication Endpoints

Authentication endpoints are automatically registered by Convex Auth:

Adding New Routes

1. Create Route Module

Create a new file in the appropriate domain folder:

// convex/yourDomain/api.ts
import { HttpRouter } from "convex/server";
import { httpAction } from "../_generated/server";

export function yourDomainRoutes(http: HttpRouter) {
  http.route({
    path: "/your-endpoint",
    method: "GET",
    handler: httpAction(async (ctx) => {
      // Implementation
      return new Response("OK", { status: 200 });
    }),
  });
}

2. Register in Main Router

Import and register in convex/http.ts:

import { yourDomainRoutes } from "./yourDomain/api";

// ... other imports

const http = httpRouter();
auth.addHttpRoutes(http);

// Register your routes
yourDomainRoutes(http);

// ... other route registrations

export default http;

3. Best Practices

  • Group by domain: Routes should be grouped by their business domain
  • Clear naming: Use descriptive function names like billingRoutes, emailRoutes
  • Documentation: Add JSDoc comments explaining what each endpoint does
  • Error handling: Always include try/catch and appropriate error responses
  • Logging: Use the logger for important events and errors
  • Type safety: Import types from _generated/api and _generated/server

Response Patterns

Success Response

return new Response(JSON.stringify(data), {
  status: 200,
  headers: {
    "Content-Type": "application/json",
  },
});

Error Response

try {
  // ... operation
} catch (error) {
  logger.error("Operation failed:", error);
  return new Response(JSON.stringify({ error: "Operation failed" }), {
    status: 500,
    headers: { "Content-Type": "application/json" },
  });
}

Cached Response (Public Endpoints)

return new Response(JSON.stringify(data), {
  status: 200,
  headers: {
    "Content-Type": "application/json",
    "Cache-Control": "public, max-age=3600", // 1 hour
    "Access-Control-Allow-Origin": "*",
  },
});

Benefits of Modular Routes

Better Organization: Routes grouped by domain instead of one large file ✅ Scalability: Easy to add new endpoints without cluttering main router ✅ Maintainability: Changes to domain routes don't affect other domains ✅ Clear Ownership: Each module owns its HTTP endpoints ✅ Testability: Easier to test individual route modules ✅ Discoverability: Clear file structure makes endpoints easy to find

Migration from Monolithic Router

If you have routes defined in a single http.ts file:

  1. Create module file: convex/domain/api.ts
  2. Move routes: Copy route definitions to the module
  3. Export function: Wrap routes in export function domainRoutes(http: HttpRouter)
  4. Update imports: Import dependencies relative to module location
  5. Register: Import and call in main http.ts
  6. Test: Verify endpoints still work correctly

On this page

Ship your startup faster. In minutes.

Get TinyKit Pro