HTTP Routes Architecture
TinyKit SaaS 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 routesMain 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 SaaS API",
description: "REST API for TinyKit SaaS 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
| Path | Method | Purpose | Module |
|---|---|---|---|
/health | GET | Health check for monitoring | Main router |
/og-settings | GET | OG image generation settings | siteSettings |
/og-image-url | GET | Custom OG image URL | siteSettings |
Webhook Endpoints
| Path | Method | Purpose | Module |
|---|---|---|---|
/stripe/webhook | POST | Stripe billing events | billing |
/resend-webhook | POST | Email delivery events | emails |
Authentication Endpoints
Authentication endpoints are automatically registered by Convex Auth:
/api/auth/*- OAuth flows, sign-in, sign-up- See Convex Auth documentation for details
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/apiand_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:
- Create module file:
convex/domain/api.ts - Move routes: Copy route definitions to the module
- Export function: Wrap routes in
export function domainRoutes(http: HttpRouter) - Update imports: Import dependencies relative to module location
- Register: Import and call in main
http.ts - Test: Verify endpoints still work correctly
Related Documentation
- API Reference - Convex function patterns
- Architecture - System design overview
- Development Guide - Development workflows