API Templates
API templates provide a Hono REST API server without a frontend. Perfect for microservices, backend services, or when you have a separate frontend application.
Available API Templates
API with Everything
Complete REST API with database and authentication.
npm create vinoflare@latest my-api --type=api-only
Features:
✅ Hono REST API framework
✅ Cloudflare D1 database with Drizzle ORM
✅ Better Auth with Discord OAuth
✅ OpenAPI documentation
✅ Testing setup with Vitest
API without Authentication
Public API with database persistence.
npm create vinoflare@latest my-api --type=api-only --no-auth
Features:
✅ Hono REST API framework
✅ Cloudflare D1 database with Drizzle ORM
❌ No authentication
✅ Public endpoints
✅ CORS configured
API without Database
Stateless API for webhooks, proxies, or external integrations.
npm create vinoflare@latest my-api --type=api-only --no-db
Features:
✅ Hono REST API framework
❌ No database
❌ No authentication
✅ Minimal footprint
✅ Fast cold starts
Project Structure
Core Concepts
Module System
API templates use a modular architecture where each feature is a self-contained module:
// src/server/modules/todos/todos.routes.ts
import { createRouter } from "@/server/core/api-builder" ;
import * as handlers from "./todos.handlers" ;
export const todosRouter = createRouter ()
. get ( "/" , handlers.getTodos)
. get ( "/:id" , handlers.getTodo)
. post ( "/" , handlers.createTodo)
. put ( "/:id" , handlers.updateTodo)
. delete ( "/:id" , handlers.deleteTodo);
// src/server/modules/todos/todos.handlers.ts
import { Context } from "hono" ;
import { db } from "@/server/db" ;
import { todos } from "@/server/db/schema" ;
export async function getTodos ( c : Context ) {
const allTodos = await db. select (). from (todos);
return c. json (allTodos);
}
export async function createTodo ( c : Context ) {
const body = await c.req. json ();
const newTodo = await db. insert (todos). values (body). returning ();
return c. json (newTodo[ 0 ], 201 );
}
Middleware Stack
// src/server/core/app-factory.ts
export function createApp () {
const app = new Hono < AppBindings >();
// Global middleware
app. use ( "*" , jsonLogger ());
app. use ( "*" , cors ());
// Conditional middleware
if (hasDatabase) {
app. use ( "*" , database ());
}
if (hasAuth) {
app. use ( "/api/*" , authGuard ());
}
// Mount routes
app. route ( "/api/todos" , todosRouter);
app. route ( "/api/users" , usersRouter);
// Error handling
app. onError (errorHandler);
return app;
}
Database Integration
Schema Definition
// src/server/db/schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" ;
export const todos = sqliteTable ( "todos" , {
id: text ( "id" ). primaryKey (). $defaultFn (() => crypto. randomUUID ()),
title: text ( "title" ). notNull (),
description: text ( "description" ),
completed: integer ( "completed" , { mode: "boolean" }). default ( false ),
userId: text ( "user_id" ). notNull (),
createdAt: integer ( "created_at" , { mode: "timestamp" })
. $defaultFn (() => new Date ()),
updatedAt: integer ( "updated_at" , { mode: "timestamp" })
. $defaultFn (() => new Date ()),
});
export type Todo = typeof todos.$inferSelect;
export type NewTodo = typeof todos.$inferInsert;
Database Queries
// Complex queries with Drizzle
import { eq, and, like, desc } from "drizzle-orm" ;
// Filter by user
const userTodos = await db
. select ()
. from (todos)
. where ( eq (todos.userId, userId));
// Search with pagination
const results = await db
. select ()
. from (todos)
. where (
and (
eq (todos.userId, userId),
like (todos.title, `%${ search }%` )
)
)
. orderBy ( desc (todos.createdAt))
. limit (limit)
. offset (offset);
// Count total
const [{ count }] = await db
. select ({ count: count () })
. from (todos)
. where ( eq (todos.userId, userId));
Authentication
Setup Better Auth
// src/server/lib/auth.ts
import { betterAuth } from "better-auth" ;
import { drizzleAdapter } from "better-auth/adapters/drizzle" ;
import { discord } from "better-auth/social-providers" ;
export const auth = betterAuth ({
database: drizzleAdapter (db, {
provider: "sqlite" ,
}),
socialProviders: {
discord: {
clientId: c.env. DISCORD_CLIENT_ID ,
clientSecret: c.env. DISCORD_CLIENT_SECRET ,
},
},
trustedOrigins: [ "http://localhost:5173" ],
});
Protected Routes
// src/server/middleware/auth-guard.ts
export function authGuard () {
return async ( c : Context , next : Next ) => {
const session = await auth.api. getSession ({ headers: c.req.raw.headers });
if ( ! session) {
return c. json ({ error: "Unauthorized" }, 401 );
}
c. set ( "user" , session.user);
c. set ( "session" , session);
await next ();
};
}
// Use in routes
const protectedRouter = createRouter ()
. use ( "*" , authGuard ())
. get ( "/profile" , async ( c ) => {
const user = c. get ( "user" );
return c. json (user);
});
Request Validation
Using Zod
import { z } from "zod" ;
import { zValidator } from "@hono/zod-validator" ;
const createTodoSchema = z. object ({
title: z. string (). min ( 1 ). max ( 100 ),
description: z. string (). optional (),
dueDate: z. string (). datetime (). optional (),
});
export const todosRouter = createRouter ()
. post ( "/" ,
zValidator ( "json" , createTodoSchema),
async ( c ) => {
const validated = c.req. valid ( "json" );
const todo = await createTodo (validated);
return c. json (todo, 201 );
}
);
Custom Validators
// Validate query parameters
const paginationSchema = z. object ({
page: z.coerce. number (). min ( 1 ). default ( 1 ),
limit: z.coerce. number (). min ( 1 ). max ( 100 ). default ( 10 ),
sort: z. enum ([ "asc" , "desc" ]). default ( "desc" ),
});
. get ( "/" ,
zValidator ( "query" , paginationSchema),
async ( c ) => {
const { page , limit , sort } = c.req. valid ( "query" );
// Use validated params
}
);
Error Handling
Global Error Handler
// src/server/core/error-handler.ts
export function errorHandler ( error : Error , c : Context ) {
console. error ( "API Error:" , error);
if (error instanceof HTTPException ) {
return c. json (
{ error: error.message },
error.status
);
}
if (error instanceof z . ZodError ) {
return c. json (
{
error: "Validation failed" ,
details: error.errors,
},
400
);
}
// Generic error
return c. json (
{ error: "Internal server error" },
500
);
}
Custom Error Classes
export class NotFoundError extends Error {
constructor ( resource : string ) {
super ( `${ resource } not found` );
this .name = "NotFoundError" ;
}
}
export class UnauthorizedError extends Error {
constructor ( message = "Unauthorized" ) {
super (message);
this .name = "UnauthorizedError" ;
}
}
// Use in handlers
export async function getTodo ( c : Context ) {
const id = c.req. param ( "id" );
const todo = await db. select (). from (todos). where ( eq (todos.id, id)). get ();
if ( ! todo) {
throw new NotFoundError ( "Todo" );
}
return c. json (todo);
}
Testing
Unit Tests
// src/server/modules/todos/todos.test.ts
import { describe, it, expect, beforeEach } from "vitest" ;
import { createApp } from "@/server/core/app-factory" ;
describe ( "Todos API" , () => {
let app : ReturnType < typeof createApp>;
beforeEach (() => {
app = createApp ();
});
it ( "should create a todo" , async () => {
const res = await app. request ( "/api/todos" , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({
title: "Test Todo" ,
description: "Test Description" ,
}),
});
expect (res.status). toBe ( 201 );
const todo = await res. json ();
expect (todo.title). toBe ( "Test Todo" );
});
it ( "should validate input" , async () => {
const res = await app. request ( "/api/todos" , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({}),
});
expect (res.status). toBe ( 400 );
const error = await res. json ();
expect (error.error). toBe ( "Validation failed" );
});
});
Integration Tests
import { testClient } from "@/server/tests/test-helpers" ;
describe ( "Todos Integration" , () => {
it ( "should handle full CRUD cycle" , async () => {
// Create
const createRes = await testClient. post ( "/api/todos" , {
json: { title: "Integration Test" },
});
const todo = await createRes. json ();
// Read
const getRes = await testClient. get ( `/api/todos/${ todo . id }` );
expect (getRes.status). toBe ( 200 );
// Update
const updateRes = await testClient. put ( `/api/todos/${ todo . id }` , {
json: { completed: true },
});
expect (updateRes.status). toBe ( 200 );
// Delete
const deleteRes = await testClient. delete ( `/api/todos/${ todo . id }` );
expect (deleteRes.status). toBe ( 204 );
});
});
OpenAPI Documentation
Generate Documentation
This generates openapi.json
with your API documentation.
Serve Documentation
// src/server/routes/docs.ts
import { swaggerUI } from "@hono/swagger-ui" ;
export const docsRouter = new Hono ()
. get ( "/" , swaggerUI ({ url: "/api/openapi.json" }))
. get ( "/openapi.json" , async ( c ) => {
const spec = await import ( "@/generated/openapi.json" );
return c. json (spec);
});
Access at: http://localhost:8787/docs
Deployment
Environment Variables
# Production secrets
wrangler secret put BETTER_AUTH_SECRET
wrangler secret put DISCORD_CLIENT_ID
wrangler secret put DISCORD_CLIENT_SECRET
Database Migrations
# Generate migration
npm run db:generate
# Apply to production
npm run db:push:prod
Deploy to Cloudflare
Common Patterns
Health Checks
app. get ( "/health" , ( c ) => {
return c. json ({
status: "healthy" ,
timestamp: new Date (). toISOString (),
version: process.env. VERSION || "1.0.0" ,
});
});
Rate Limiting
import { rateLimiter } from "hono-rate-limiter" ;
app. use ( "/api/*" , rateLimiter ({
windowMs: 60 * 1000 , // 1 minute
limit: 100 , // 100 requests per minute
standardHeaders: "draft-6" ,
keyGenerator : ( c ) => {
return c.req. header ( "CF-Connecting-IP" ) || "anonymous" ;
},
}));
CORS Configuration
import { cors } from "hono/cors" ;
app. use ( "*" , cors ({
origin : ( origin ) => {
const allowed = [
"http://localhost:5173" ,
"https://myapp.com" ,
];
return allowed. includes (origin) ? origin : allowed[ 0 ];
},
credentials: true ,
allowMethods: [ "GET" , "POST" , "PUT" , "DELETE" , "OPTIONS" ],
allowHeaders: [ "Content-Type" , "Authorization" ],
}));
Next Steps