Skip to content

ivan-yuldashev/hono-quickstart

Repository files navigation

Hono Quickstart

An opinionated, production-focused starter for Hono. This template provides an end-to-end type-safe stack, featuring a Modular Architecture, Drizzle ORM, PostgreSQL, Zod, and automated OpenAPI documentation.

Included

  • Structured logging with pino / hono-pino
  • Documented / type-safe routes with @hono/zod-openapi
  • Interactive API documentation with scalar / @scalar/hono-api-reference
  • Convenience methods / helpers to reduce boilerplate with stoker
  • Type-safe schemas and environment variables with zod
  • Single source of truth database schemas with PostgreSQL, Drizzle ORM and drizzle-zod
  • Testing with Vitest
  • Sensible editor, formatting and linting settings with @antfu/eslint-config
  • Advanced Authentication: Dual JWT (Access Token in body + Refresh Token in httpOnly Cookie) with token rotation.
  • Modular Architecture: Domain-driven structure.
  • Graceful shutdown with Terminus
  • Unified error format
  • Context-Aware Transactions: Clean transaction management using AsyncLocalStorage (no argument drilling).

Philosophy

This template is built with a few core principles in mind:

Type-Safety End-to-End: From database schemas (Drizzle + Zod) to API endpoints (@hono/zod-openapi), types are the source of truth, reducing runtime errors.

Modular Architecture: The project is organized by domain modules (e.g., auth, users, tasks) rather than technical layers. This Vertical Slice approach keeps related logic (routes, services, schemas) together, making the codebase easier to navigate and scale.

Developer Experience (DX): Fast setup (degit), automated testing with a real database (Vitest + Docker), and smart linting (@antfu/eslint-config) are prioritized.

Production-Ready: Includes essentials like structured logging (pino), graceful shutdown (terminus), and environment validation (zod) from the start.

Setup

Clone this template without git history

npx degit ivan-yuldashev/hono-quickstart my-api
cd my-api

Create .env file

cp .env.example .env

Note: See .env.example for a list of all required environment variables. Typesafe env is defined in env.ts. The application will not start if any required environment variables are missing.

Install dependencies

corepack enable
pnpm install

Run the database

docker-compose \-f docker-compose.dev.yml up \-d

Generate database migrations

pnpm db:generate

Run database migrations

pnpm db:migrate

Run

pnpm dev

Lint

pnpm lint

Test

pnpm test

Code Tour

The project uses a Modular Architecture. Instead of scattering code across global controllers or routes folders, everything related to a specific domain is encapsulated within src/modules.

Directory Structure

src/
|-- app/ // Core Hono setup, middleware & AppType
|-- infrastructure/ // Technical concerns (DB, Logger, Env)
|-- modules/ // Business Domains (Vertical Slices)
| |-- auth/ // Example module
| | |-- auth.routes.ts
| | |-- auth.service.ts
| | |-- ...
| |-- users/
| |-- tasks/
|-- shared/ // Shared Utilities, Types, Constants
  • src/app: Contains the core Hono app creation, global middleware registration, and module aggregation.
  • src/infrastructure: Contains shared technical concerns: database connection (Drizzle), config, logger, Service Factory, and base classes.
  • src/modules: Contains the business domains. Each module is self-contained.
  • src/shared: Contains shared code, such as types, constants, and utility functions used across modules.

RPC Client Support: All app routes are grouped together and exported into single type as AppType in src/app/app.ts for use in RPC / hono/client.

Service Layer

The application employs a hybrid service architecture, combining automated CRUD with custom domain logic.

1. Dynamic Service Factory (BaseService)

Found in src/infrastructure/services/helpers/create-services.ts. This factory automatically creates a BaseService for every Drizzle table defined in the schema and injects them into the Hono context via middleware. Usage Example:

You can access any auto-generated service directly from the context using c.get('services').

export const list: AppRouteHandler<ListRoute> = async (c) = {
const { limit, offset } = c.req.valid('query');

// Access the dynamic service for 'tasks'
const { tasks } = c.get('services');

// Uses BaseService.find() which handles pagination automatically
const data = await tasks.find({ limit, offset });

return c.json(data, HttpStatusCodes.OK);
};

BaseService Methods:

The auto-generated services provide a comprehensive set of methods for interacting with the database.

Read Operations:

  • find(params):
    • Input: { limit, offset, where?, with?, orderBy? }
    • Output: Promise<Page<Data>> ({ docs, total })
    • Description: Combines repository calls (findBy and count) in parallel to return a paginated response.
  • findById(id):
    • Input: IdType<Data>
    • Output: Promise<Data | undefined>
    • Description: Fetches a single record by its primary key.
  • count(params):
    • Input: CountParams (e.g. { where })
    • Output: Promise<number>
    • Description: Returns the count of records matching the criteria.

Write Operations:

  • create(data):
    • Input: WithoutBaseFields<Data>
    • Output: Promise<Data | undefined>
    • Description: Validates and inserts a new record into the database.
  • updateById(id, raw):
    • Input: IdType<Data>, FullPartial<WithoutBaseFields<Data>>
    • Output: Promise<Data | undefined>
    • Description: Updates a record by ID. Automatically sanitizes the input (removes undefined keys) before passing data to the repository, making it perfect for PATCH requests.
  • update(raw, where):
    • Input: FullPartial<WithoutBaseFields<Data>>, SQL
    • Output: Promise<Data[]>
    • Description: Bulk updates records matching the where clause. Also sanitizes input.
  • deleteById(id):
    • Input: IdType<Data>
    • Output: Promise<Data | undefined>
    • Description: Deletes a single record by its ID.
  • delete(where):
    • Input: SQL
    • Output: Promise<Data[]>
    • Description: Bulk deletes records matching the where clause.

2. Domain Services (Custom Logic)

For complex business logic that goes beyond simple CRUD (e.g., Authentication, Transactions), specific services are defined within their modules (e.g., src/modules/auth/auth.service.ts).

  • Example (AuthService):
    • Orchestrates operations between UserRepository and TokenRepository.
    • Login/Register: Handles password verification (using safe comparison) and hashing.
    • Token Rotation: Implements Refresh Token Rotation. If a revoked token is used, the system detects a potential breach and revokes all tokens for that user (Reuse Detection).

3. Transaction Management

The project uses a Context-Aware Transaction pattern implemented via Node.js AsyncLocalStorage. This solves the common problem of "argument drilling" (passing tx objects down the call stack).

How it works: You wrap your business logic in a transaction(async () => { ... }) helper.

Automatic Context: Repositories automatically detect if they are running inside a transaction scope and switch to the transaction executor.

Isolation: Transaction contexts are isolated to the current request scope.

Example:

// src/modules/auth/auth.service.ts
import { transaction } from '@/shared/lib/transaction-manager';

public async refresh(token: string) {
// ... validation logic ...

// All repository calls inside this block share the same transaction
return await transaction(async () => {
await this.tokenRepository.revokeOld(token);
const newTokens = await generateTokens(user);
await this.tokenRepository.create(newTokens);
return newTokens;
});
}

Testing

This project uses Vitest for testing. Test files are located in the tests directory or colocated within modules (depending on preference).

When you run the test command, a global setup script (tests/global-setup.ts) is executed. This script automatically:

  1. Starts a PostgreSQL database in a Docker container using the docker-compose.test.yml file.
  2. Applies the latest database schema using Drizzle.
  3. After the tests complete, a teardown script automatically stops and removes the Docker container.

You can run the tests with the following command:

pnpm test

Features

Dual JWT Authentication

The application implements a robust authentication strategy using two tokens:

  1. Access Token: Short-lived, returned in the response body. Used for authorizing API requests via the Authorization: Bearer header.
  2. Refresh Token: Long-lived, stored in a secure httpOnly cookie. Used to obtain a new Access Token when the current one expires.

Rotation: The system supports Refresh Token Rotation to detect token reuse and enhance security.

Graceful Shutdown

The application uses Terminus to gracefully shut down the server. This ensures that all active connections are closed before the process exits, preventing data loss and ensuring a clean shutdown.

Environment Variable Validation

The application uses Zod to validate environment variables at startup. This ensures that all required environment variables are present and have the correct type, preventing runtime errors. The environment variable schema and validation logic can be found in src/infrastructure/config/env.ts.

Unified Error Format

The application uses a unified error format for all API responses. This makes it easier for clients to handle errors in a consistent way. The error handling logic can be found in src/infrastructure/http/helpers/on-error.ts.

Endpoints

Path Description
System
GET /health Health check
GET /openapi Open API Specification (JSON)
GET /doc Scalar API Documentation
Auth
POST /register Register a new user
POST /login Login a user (Returns Access Token + Sets Refresh Cookie)
POST /logout Logout a user (Clears Refresh Cookie)
POST /refresh-token Refresh Access Token using the cookie
Tasks
GET /tasks List all tasks
POST /tasks Create a task
GET /tasks/{id} Get one task by id
PATCH /tasks/{id} Patch one task by id
DELETE /tasks/{id} Delete one task by id

Deployment

This starter is configured to run as a Node.js server (see src/index.ts). You can build a production-ready Docker image and deploy it to any platform that supports Docker containers.

A Dockerfile is not included, but you can easily add one based on a lightweight Node.js image.

Contributing

Contributions are welcome! Please feel free to open an issue or submit a pull request.

  1. Fork the repository.
  2. Create a new branch (git checkout -b feature/your-feature).
  3. Make your changes.
  4. Run tests (pnpm test) and lint (pnpm lint).
  5. Commit your changes (git commit -m 'Add some feature').
  6. Push to the branch (git push origin feature/your-feature).
  7. Open a Pull Request.

Acknowledgements

This project is based on the original work of w3cj/hono-open-api-starter.

References


```

```

About

An opinionated, production-focused starter for Hono. This template provides an end-to-end type-safe stack, featuring a Modular Architecture, Context-Aware Transactions, Drizzle ORM, PostgreSQL, Zod, and automated OpenAPI documentation.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors