- TypeScript 92.9%
- JavaScript 7.1%
|
|
||
|---|---|---|
| scripts | ||
| src | ||
| template | ||
| tests | ||
| .gitignore | ||
| CHANGELOG.md | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
Introduction
A template for a HTTP server built with Hono and Bun. Provides an opinionated HTTP middleware pipeline, structured request logging, OpenAPI documentation with Scalar UI, and a Bun server host with graceful shutdown. Designed to be used standalone or as the backend in a monorepo alongside one or more @temir.ra/create-hono-spa SPA packages mounted as route groups.
Table of Contents
Quick Start
# placeholder:
# <TEMPLATE_PACKAGE: @temir.ra/create-hono-server
# <TEMPLATE_NAME: @temir.ra/hono-server
# <NEW_PACKAGE: <NEW_PACKAGE>
# is used as:
# - the path where the package is created
# - the "name" field in the generated package.json
# <@_VERSION: <@_VERSION>
# pinned version
bun info "@temir.ra/create-hono-server" version
bun create --no-install --no-git "@temir.ra/hono-server<@_VERSION>" <NEW_PACKAGE>
# latest
# clear the cache to pick up the latest version
bun pm cache rm
bun create --no-install --no-git "@temir.ra/hono-server" <NEW_PACKAGE>
# templates only copy files, run install and any setup scripts manually
cd <NEW_PACKAGE>
bun install
Documentation
The following sections explain the configurations and conventions baked into the generated package. Useful when adapting it to fit specific needs.
template/
Official documentation provided by the Hono team is a great resource.
Architecture
The generated server is organized around three roles:
main.ts → createAppHost() → startServerHost()
main.ts is the entry point and orchestrator. It calls createAppHost() to configure the Hono application, then passes the result to startServerHost(), which hands it to Bun.serve() and manages the process lifecycle.
- App host (
app-host.ts) - owns the application: what middleware runs, which routes and endpoint groups are mounted, what the API looks like. - Server host (
server-host.ts) - owns the server: which port it listens on, how it shuts down on process signals. main.ts- wires them together; the natural place to compose the application.
// main.ts
const appHost = createAppHost<AppEnv>({
basePath: env.BASE_PATH,
logging: { logLevel: env.LOG_LEVEL },
exposeOpenApi: env.EXPOSE_OPENAPI,
endpointGroups: [myRoutes],
});
startServerHost({ port: env.PORT, appHost });
src/env.ts
Validates and exports the server configuration from environment variables using Zod. Parsed once at startup — if any required variable fails validation, the process exits with an error.
| Variable | Type | Default | Description |
|---|---|---|---|
NODE_ENV |
development | production | test |
development |
Runtime environment |
PORT |
number | 7200 |
Port the server listens on |
BASE_PATH |
string (optional) | — | Base path prefix for all routes (e.g. api) |
LOG_LEVEL |
LogLevel name |
WARNING |
Global log level |
LOG_LEVEL__<scope> |
LogLevel name |
— | Per-scope log level override; one variable per scope (e.g. LOG_LEVEL__request=TRACE) |
EXPOSE_OPENAPI |
true | 1 | anything else |
false |
Whether to register OpenAPI spec and Scalar UI endpoints |
LOG_LEVEL and LOG_LEVEL__<scope> accept the LogLevel enum member names: NONE, ERROR, WARNING, INFO, TRACE.
The generated .env.development file provides sensible defaults for local development:
PORT=7200
BASE_PATH=
EXPOSE_OPENAPI=true
LOG_LEVEL=WARNING
LOG_LEVEL__request=TRACE
Two exports are available for use in main.ts:
env— the validated config objectgetLogLevelPerScope()— reads allLOG_LEVEL__<scope>variables and returns aRecord<string, LogLevel>
src/app-host.ts
createAppHost(options) constructs and returns a configured Hono<AppEnv> instance.
Built-in middleware pipeline (applied in this order):
| Middleware | Purpose |
|---|---|
Logging |
Attaches a scoped, leveled log function factory to c.var.getLogFunction |
requestId |
Attaches a unique request ID to c.var.requestId |
RequestLogger |
Logs method and path at TRACE level using the request scope |
compress |
Gzip/Brotli response compression |
secureHeaders |
Adds security response headers |
Built-in endpoints:
| Path | Description |
|---|---|
/health |
Returns ok with status 200 |
/buildinfo |
Returns the contents of buildinfo.txt |
/<openApiPath> |
OpenAPI JSON spec (default path: openapi) |
/<openApiUiPath> |
Scalar API reference UI (default path: scalar) |
Options:
type AppHostOptions<E extends Env> = {
basePath?: string; // base path prefix for all routes
logging: LoggingOptions; // log level configuration; see Logging middleware
exposeOpenApi: boolean; // whether to register OpenAPI spec and Scalar UI endpoints
middleware?: MiddlewareHandler<E>[]; // additional middleware, applied after built-ins
endpointGroups?: Hono<E>[]; // Hono sub-apps mounted at '/'
openApi?: {
path?: string; // OpenAPI spec path (default: 'openapi')
uiPath?: string; // Scalar UI path (default: 'scalar')
title?: string;
description?: string;
};
}
AppEnv
export interface AppEnv extends Env {
Variables: LoggingVariables
}
Extends Hono's Env with LoggingVariables, making c.var.getLogFunction and c.var.requestId available throughout. Exported to be extended when additional context variables are needed:
interface MyEnv extends AppEnv {
Variables: AppEnv['Variables'] & {
user: User;
}
}
const appHost = createAppHost<MyEnv>({ ... });
const myRoutes = new Hono<MyEnv>();
myRoutes.get('/profile', (c) => c.json(c.var.user));
Adding endpoints
const myRoutes = new Hono<AppEnv>();
myRoutes.get('/items', (c) => c.json({ items: [] }));
const appHost = createAppHost<AppEnv>({
endpointGroups: [myRoutes],
});
Mounting a SPA
Pass a createSpa() result directly as an endpoint group. See @temir.ra/create-hono-spa for details.
import { createSpa } from '@scope/my-spa/spa';
const appHost = createAppHost<AppEnv>({
basePath: 'api/',
endpointGroups: [createSpa({ basePath: 'api' })],
});
src/server-host.ts
startServerHost(options) wraps Bun.serve() and registers SIGINT/SIGTERM handlers for graceful shutdown.
type ServerHostOptions = {
port: number;
appHost: Hono<AppEnv>;
stopCallback?: (server: Bun.Server<undefined>) => void;
}
On SIGINT or SIGTERM: calls stopCallback (if provided), stops the Bun server, then exits with code 0.
src/middleware/logging/
Logging(options)
Attaches a getLogFunction(scope?) factory to the request context as c.var.getLogFunction. Call it in any handler or middleware to get a scoped log function:
const log = c.var.getLogFunction('my-scope');
log(LogLevel.INFO, 'Processing request', { id });
Log output format: [<ISO timestamp>] [<requestId>] [<scope>] [<LEVEL>] <message>
Options:
type LoggingOptions = {
logLevel: LogLevel; // global default level
logLevelPerScope?: Record<string, LogLevel>; // per-scope overrides
}
LogLevel enum: NONE | ERROR | WARNING | INFO | TRACE
The generated .env.development configures WARNING globally and TRACE for the request scope.
RequestLogger()
Logs <status> <METHOD> <duration>ms <path>[?<query>] at TRACE level using the request scope. Applied after requestId and Logging so all three are available. Duration is measured from when the middleware is entered to when the response is sent.
DevOps
bun update
bun install
bun run clean
bun run build
bun run tests
# see publish section for publish instructions
Change Management
- Create a new branch for the change.
- Make the changes and commit.
- Bump the version in
package.json. - Add an entry for the new version in
CHANGELOG.md. - Pull request the branch.
- Ensure package artifacts are current.
- Publish.
Publish
Publish to the public npm registry.
# authenticate
npm login
# publish
bun publish --registry https://registry.npmjs.org/ --access public