A modern, type-safe API starter template built with Hono, TypeScript, Drizzle ORM, Turso (libSQL), and Zod validation. This template follows best practices for building maintainable, production-ready APIs with a focus on developer experience and code quality.
- 🚀 Hono - Ultra fast web framework for the Edges
- 🔍 Type Safety - End-to-end type safety with TypeScript
- 📝 OpenAPI Documentation - Automatic OpenAPI doc generation with
@hono/zod-openapi - 🔄 Database ORM - Drizzle ORM with Turso/libSQL integration
- ✅ Validation - Request validation with Zod
- 📊 Logging - Structured logging with Pino
- 🧪 Environment Management - Type-safe environment variables with validation
- 🔐 Security - Built-in security best practices
- 📦 Modern JS - ESM-first approach with latest Node.js features
- 🛠️ Developer Experience - Hot reloading, linting, and formatting
- 🏗️ Feature-based Organization - Clean, scalable project structure
- 🚦 Error Handling - Centralized error handling with consistent responses
- 🧩 Modular Design - Separation of concerns for better maintainability
├── drizzle.config.ts # Drizzle ORM configuration
├── env.example # Example environment variables
├── eslint.config.js # ESLint configuration
├── LICENSE # Project license
├── package.json # Project dependencies
├── src/
│ ├── app.ts # App configuration
│ ├── db/
│ │ ├── index.ts # Database client setup
│ │ ├── migrations/ # Database migrations
│ │ └── schema/ # Database schema definitions
│ │ ├── index.ts # Re-exports all schemas
│ │ └── tasks.schema.ts
│ ├── env.ts # Environment configuration
│ ├── index.ts # Entry point
│ ├── lib/ # Shared utilities
│ │ ├── configure-open-api.ts
│ │ ├── constants.ts
│ │ ├── create-app.ts
│ │ ├── http-status-codes.ts
│ │ ├── http-status-phrases.ts
│ │ └── types.ts
│ ├── middlewares/ # Hono middleware
│ │ ├── index.ts
│ │ ├── not-found.ts
│ │ ├── on-error.ts
│ │ ├── pino-logger.ts
│ │ └── serve-emoji-favicon.ts
│ ├── openapi/ # OpenAPI utilities and schemas
│ │ ├── default-hook.ts
│ │ ├── helpers/
│ │ │ ├── json-content-one-of.ts
│ │ │ ├── json-content-required.ts
│ │ │ ├── json-content.ts
│ │ │ ├── one-of.ts
│ │ │ └── types.ts
│ │ └── schemas/
│ │ ├── create-error-schema.ts
│ │ ├── create-message-object.ts
│ │ ├── get-params-schema.ts
│ │ ├── id-params.ts
│ │ ├── id-uuid-params.ts
│ │ └── slug-params.ts
│ └── routes/ # API routes
│ ├── index.route.ts # Main router
│ └── tasks/ # Feature-based route organization
│ ├── tasks.handlers.ts
│ ├── tasks.index.ts
│ ├── tasks.routes.ts
│ └── tasks.services.ts
└── tsconfig.json # TypeScript configuration
- Node.js 18+
- pnpm (recommended)
- Turso CLI (for database setup)
- Clone the repository:
git clone https://github.com/yourusername/hono-api-starter.git
cd hono-api-starter- Install dependencies:
pnpm install- Set up environment variables
cp .env.example .env- Configure your database:
turso db create my-app-db
turso db tokens create my-app-db- Update your
.envfile with the Turso database URL and token:
DATABASE_URL=libsql://your-db-url.turso.io
DATABASE_AUTH_TOKEN=your-token- Run migrations:
pnpm db:migrateWarning
There's a known issue with Drizzle where imports with .js extensions in TypeScript projects can cause "Cannot find module" errors when running migrations. The db:generate script in this project already implements the necessary workaround:
NODE_OPTIONS='--import tsx' drizzle-kit generateSee Drizzle GitHub issue #2705 for more details.
- Start the development server:
pnpm run devThe API will be available at http://localhost:8080.
The API is documented using OpenAPI 3.0 specifications, which are automatically generated from your route definitions and Zod schemas. This provides:
- Interactive API documentation
- Request/response schema validation
- Type safety between documentation and implementation
Documentation is available at:
/api-docs- Scalar interface for exploring and testing the API The OpenAPI configuration is set up insrc/lib/configure-open-api.tsand integrates with your route definitions using@hono/zod-openapi.
The application uses Zod to validate environment variables, ensuring type safety and validation at runtime:
| Variable | Description | Default | Required |
|---|---|---|---|
| PORT | Server port | 8080 | No |
| NODE_ENV | Environment | development | No |
| LOG_LEVEL | Logging level (fatal, error, warn, info, debug, trace) | debug | Yes |
| DATABASE_URL | Turso database URL | - | Yes |
| DATABASE_AUTH_TOKEN | Turso auth token | - | Production only |
Invalid environment variables will cause the application to exit with a helpful error message.
Routes follow a feature-based organization:
routes/
├── index.route.ts # Main router entry point
└── tasks/ # Feature folder
├── tasks.index.ts # Route definitions and OpenAPI specs
├── tasks.routes.ts # Route configurations
├── tasks.handlers.ts # Request handlers
└── tasks.services.ts # Business logic and data access
The application uses a centralized error handling system through the onError middleware, providing consistent error responses:
{
"message": "Error message",
"stack": "Error stack (development only)"
}Database operations are abstracted through service functions:
// Example service function
export async function getTaskById(id: number) {
return await db.query.tasks.findFirst({
where: eq(tasks.id, id)
})
}- Create a new folder in
routes/for your feature (e.g.,users/) - Create the following files:
users.index.ts- Main entry point for the feature's routesusers.routes.ts- Route definitions with OpenAPI specsusers.handlers.ts- Request handlersusers.services.ts- Business logic and data access
Register your routes in app.ts:
// app.ts
import users from '@/routes/users/users.index.js'
const routes = [index, tasks, users]
routes.forEach((route) => {
app.route('/', route)
})The project uses Drizzle ORM with Turso (libSQL) for database operations. This provides a type-safe approach to database access with a clean API.
- Create a new schema file in
src/db/schema/:
// src/db/schema/users.schema.ts
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
export const users = sqliteTable('users', {
id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: integer('created_at', { mode: 'timestamp' })
.$defaultFn(() => new Date()),
updatedAt: integer('updated_at', { mode: 'timestamp' })
.$defaultFn(() => new Date())
.$onUpdate(() => new Date()),
})
// Create Zod schemas for type validation and OpenAPI documentation
export const selectUsersSchema = createSelectSchema(users)
export const insertUsersSchema = createInsertSchema(users, {
name: schema => schema.min(1).max(255),
email: schema => schema.email(),
})
.omit({ id: true, createdAt: true, updatedAt: true })- Export your schema in
src/db/schema/index.ts:
export * from './tasks.schema.js'
export * from './users.schema.js'- Generate migrations:
pnpm db:generate- Apply migrations:
pnpm db:migrateService functions encapsulate database operations and business logic:
import { eq } from 'drizzle-orm'
import type { User } from '@/db/schema'
// src/routes/users/users.services.ts
import { db } from '@/db'
import { users } from '@/db/schema'
export async function getUserById(id: number): Promise<User | null> {
const [user] = await db
.select()
.from(users)
.where(eq(users.id, id))
return user || null
}
export async function createUser(data: Omit<User, 'id' | 'createdAt' | 'updatedAt'>) {
const [user] = await db
.insert(users)
.values(data)
.returning()
return user
}pnpm run build
pnpm run startThis template works well with:
- Node.js environments
- Docker containers
- Serverless platforms (with minor adjustments)
Contributions are welcome! Please feel free to submit a Pull Request.
Looking for PostgreSQL support? Check out our companion project hono-neon-starter (coming soon).
This project was inspired by hono-open-api-starter by CJ. Thanks for the excellent foundation!
This project is licensed under the MIT License - see the LICENSE file for details.