Server
Hono service bootstrap with health checks, request IDs, and structured logging
The @lawhive/framework-server package provides a composable service builder for Hono applications with standard infrastructure baked in: health checks, request ID propagation, and structured logging.
Installation
bun add @lawhive/framework-serverQuick Start
import { createService } from "@lawhive/framework-server/service"
import { Hono } from "hono"
const { log, bootstrap } = await createService({
name: "my-service",
version: "1.0.0",
})
const { app } = await bootstrap(new Hono())
.mount(({ app, log }) => {
app.get("/hello", (c) => c.json({ message: "Hello" }))
})
.build()
export default appThis gives you out of the box:
GET /— Service metadata (name, version, environment, instance ID)GET /health— Liveness probe ({ status: "ok" })GET /health/ready— Readiness probe with custom checks- Request ID middleware (generates or propagates
X-Request-Id) - Structured logging via LogTape with wide event middleware
createService
The entry point for bootstrapping a service. Returns a log instance and a bootstrap function.
const { log, bootstrap } = await createService({
name: "my-service",
version: process.env.VERSION ?? "local",
environment: process.env.NODE_ENV, // defaults to NODE_ENV
instanceId: process.env.INSTANCE_ID, // defaults to HOSTNAME or "local"
region: process.env.REGION, // defaults to "local"
options: {
health: {
readiness: {
checks: {
database: async () => {
await prisma.$queryRaw`SELECT 1`
},
},
},
rootMeta: {
links: {
docs: "/api-reference",
rpc: "/rpc",
},
},
},
},
})Config
| Option | Type | Default | Description |
|---|---|---|---|
name | string | — | Service name (required) |
version | string | — | Service version (required) |
environment | string | NODE_ENV | Deployment environment |
instanceId | string | HOSTNAME | Instance identifier |
region | string | "local" | Deployment region |
options | ServiceOptions | — | Health, logging, and request ID config |
Service Builder
The bootstrap function returns a builder that lets you compose modules and mount routes:
const { app } = await bootstrap(new Hono())
.use(myCustomModule) // Add capabilities
.mount(({ app, log }) => { // Wire routes
app.get("/tasks", (c) => c.json([]))
})
.build().use(module)
Adds a service module that extends the service context with new capabilities. Modules receive the current context and return new capabilities:
type ServiceModule<TNeeds, TAdds> = (
ctx: ServiceContext<TNeeds>,
) => Awaitable<TAdds>.mount(fn)
Mounts route handlers. Receives the full accumulated context from all modules:
.mount(({ app, log, meta, getRequestId }) => {
app.get("/tasks", (c) => {
const requestId = getRequestId(c)
log.info("Listing tasks", { requestId })
return c.json([])
})
}).build()
Executes all modules and mounts in order, returning the final ServiceContext.
Standard Capabilities
After createService, the builder starts with these capabilities:
| Capability | Type | Description |
|---|---|---|
app | Hono | The Hono app instance |
meta | ServiceMeta | Service metadata (name, version, etc.) |
log | Log | LogTape logger |
standard | HonoStandard | Health route handlers |
getRequestId | (c: Context) => string | Extract request ID from context |
Health Checks
Liveness
GET /health always returns 200:
{
"status": "ok",
"timestamp": "2026-03-10T12:00:00.000Z",
"request_id": "abc-123"
}Readiness
GET /health/ready runs all configured checks:
const { log, bootstrap } = await createService({
name: "my-service",
version: "1.0.0",
options: {
health: {
readiness: {
checks: {
database: async () => {
await prisma.$queryRaw`SELECT 1`
},
redis: async () => {
await redis.ping()
},
},
},
},
},
})Returns 200 when all checks pass, 503 if any fail:
{
"status": "ready",
"timestamp": "2026-03-10T12:00:00.000Z",
"request_id": "abc-123",
"checks": {
"database": { "status": "ok" },
"redis": { "status": "ok" }
}
}Request IDs
The standard middleware automatically:
- Reads
X-Request-Idfrom incoming request headers - Generates a UUID if none provided
- Attaches it to the Hono context
- Returns it in the response
X-Request-Idheader
Access via getRequestId(c) in route handlers.
Logging
createService sets up structured logging via LogTape. The log instance is available both at the service level and in mount callbacks:
const { log, bootstrap } = await createService({ name: "my-service", version: "1.0.0" })
log.info("Service starting")
await bootstrap(new Hono())
.mount(({ app, log }) => {
log.info("Mounting routes")
app.get("/", (c) => c.json({ ok: true }))
})
.build()The request logging middleware automatically emits wide events for each request with timing, status code, and request ID.
Full Example
import { createService } from "@lawhive/framework-server/service"
import { Hono } from "hono"
import { cors } from "hono/cors"
import prisma from "./db"
const { log, bootstrap } = await createService({
name: "task-service",
version: process.env.VERSION ?? "local",
options: {
health: {
rootMeta: {
links: { rpc: "/rpc", docs: "/api-reference" },
},
readiness: {
checks: {
database: async () => { await prisma.$queryRaw`SELECT 1` },
},
},
},
},
})
const { app } = await bootstrap(new Hono())
.mount(({ app }) => {
app.use("/*", cors({ origin: process.env.CORS_ORIGIN || "" }))
app.get("/tasks", async (c) => {
const tasks = await prisma.task.findMany()
return c.json(tasks)
})
})
.build()
export default app