Skip to content

feat(template): dependency-checking /health endpoint (#54)#92

Merged
PAMulligan merged 7 commits into
mainfrom
54-add-health-check-endpoint-with-dependency-status
May 14, 2026
Merged

feat(template): dependency-checking /health endpoint (#54)#92
PAMulligan merged 7 commits into
mainfrom
54-add-health-check-endpoint-with-dependency-status

Conversation

@PAMulligan
Copy link
Copy Markdown
Contributor

Summary

Replaces the static { status: 'ok' } health endpoint with a real liveness check that pings the database, reports version + uptime, and returns 503 when a dependency is down. Closes #54.

Response shape (HTTP 200 healthy, 503 unhealthy):

{
  "status": "healthy",
  "version": "0.0.1",
  "uptime": 3600,
  "timestamp": "2026-04-08T12:00:00Z",
  "requestId": "01HXYZ...",
  "checks": { "database": "connected" }
}

Changes:

  • setup-project.sh Workers + Node templates: extract src/routes/health.ts and src/db/ping.ts; mount via app.route('/health', healthRoutes); add APP_VERSION + HEALTH_DB_TIMEOUT_MS to env templates and wrangler.toml [vars]
  • New shared src/db/client.ts (postgres singleton) so seed.ts and future code stop building their own connections
  • examples/todo-api-cloudflare mirrors the design with a D1-flavored pingDatabase
  • Tests cover the full matrix: healthy/disconnected/timeout/throw/version/uptime/requestId — 7 cases each for Node, Workers, and the D1 example (mock pingDatabase, no real Postgres needed in CI)

Design decisions (full rationale in docs/plans/2026-05-14-health-endpoint-dependency-checks.md):

  • Uptime on Workers = Date.now() - startTime at module load (isolate uptime — honest about Workers semantics, zero cost)
  • Version via APP_VERSION env var (one pattern across Workers + Node; deploys override per env)
  • 2 s DB ping timeout (well under k8s livenessProbe default; configurable via HEALTH_DB_TIMEOUT_MS)
  • Handler never 5xxs: internal errors are caught and surfaced as 503 + database: 'disconnected', so load balancers don't treat them as "needs restart"
  • pingDatabase takes the binding directly (Hyperdrive | undefined, D1Database | undefined) instead of a Context — decouples db/ping.ts from Hono and avoids Context<Bindings> variance issues

Test Plan

  • examples/todo-api-cloudflare: 19/19 tests pass, typecheck clean
  • Generated Node project (bash scripts/setup-project.sh /tmp/x --node): 7/7 health tests pass
  • Generated Workers project (bash scripts/setup-project.sh /tmp/x --cloudflare): 7/7 health tests pass
  • bash -n scripts/setup-project.sh clean after every edit
  • CI: Validate Structure, Node 20 / Node 22 Compatibility, Check Markdown Links

Out of scope (follow-up)

  • Pre-existing template typecheck gaps surfaced by the smoke test, not introduced by this PR: the shared templates/shared/tsconfig.json is missing "types": ["node"], and the Cloudflare branch of setup-project.sh doesn't install @cloudflare/workers-types. The existing pre-change index.ts already used process / console / setTimeout and D1Database / Hyperdrive without these types. Generated tests run fine (vitest doesn't typecheck by default); fixing the strict-typecheck path on generated projects is its own change.
  • /health/live vs /health/ready split — single endpoint matches the issue spec.
  • Auto-injecting APP_VERSION from package.json at build time — env var pattern is the contract; build-time substitution is a follow-up.

🤖 Generated with Claude Code

Paul Mulligan and others added 7 commits May 14, 2026 12:49
Captures the design decisions (isolate uptime on Workers, APP_VERSION env
var, extracted routes/health.ts, 2s DB ping timeout, 503 on any dependency
failure) and breaks implementation into seven task-scoped commits across
both setup-project.sh templates and the todo-api-cloudflare example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds src/db/client.ts with a postgres-js singleton and shared drizzle
instance reading DATABASE_URL at module load. seed.ts now imports from
client.ts instead of constructing its own connection. The client module
is generated for both templates because seed.ts runs under tsx (Node)
regardless of deploy target; the Workers runtime never imports it.

First step toward the dependency-checking /health endpoint (#54).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generates src/db/ping.ts with a platform-specific body. The Workers
variant builds a per-request postgres client over the HYPERDRIVE
binding's connection string and runs SELECT 1; the Node variant uses
the shared client singleton from db/client.ts. Both return
'connected' | 'disconnected' and never throw — the caller is the
/health endpoint, which must never 5xx the prober.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the inline app.get('/health', ...) in both Workers and Node
templates with a mounted routes/health.ts handler. The new handler:

- Pings the database via pingDatabase() with a configurable timeout
  (HEALTH_DB_TIMEOUT_MS, default 2000 ms)
- Reports overall status as 'healthy' / 'unhealthy' and returns 503
  when any dependency is disconnected
- Includes version (from APP_VERSION env), uptime (seconds since module
  load — isolate uptime on Workers, process uptime on Node), timestamp,
  requestId, and a checks.database field
- Never throws to the caller: any internal error is caught and reported
  as 'disconnected' + 503, so load balancers never see a 5xx that wasn't
  caused by an actual dependency outage

Workers Bindings now declare APP_VERSION and HEALTH_DB_TIMEOUT_MS so the
template type-checks against wrangler.toml [vars] entries (added next).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lates

The new /health endpoint reads these from c.env (Workers) or process.env
(Node). Adds them to:

- .dev.vars.example (Workers local dev)
- .env.example (Node local dev)
- wrangler.toml [vars], [env.staging.vars], [env.production.vars]

APP_VERSION mirrors package.json so generated projects ship with a real
value out of the box; HEALTH_DB_TIMEOUT_MS defaults to 2000 ms so the
DB ping fails fast under network partition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… error paths

Replaces the standalone Hono fixture in tests/unit/health.test.ts with
a test that mounts the real healthRoutes inside a Hono parent + requestId
middleware (mirroring index.ts wiring). pingDatabase is mocked via
vi.mock so no real postgres is needed in CI.

Coverage (both Workers and Node test bodies):
- 200 + healthy when DB connected
- version sourced from APP_VERSION (c.env on Workers, process.env on Node)
- numeric uptime >= 0
- 503 + unhealthy when DB disconnected
- 503 on timeout via fake-timer-advanced Promise.race
- 503 on thrown error (never 500)
- requestId preserved in body and X-Request-Id header

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the new health endpoint design in the example:

- examples/todo-api-cloudflare/api/src/db/ping.ts (D1: SELECT 1 via
  c.env.DB.prepare('SELECT 1').first())
- examples/todo-api-cloudflare/api/src/routes/health.ts (same handler
  shape as the Workers template, but typed for the example's D1 binding)
- src/index.ts: drop inline /health handler, mount healthRoutes
- wrangler.toml [vars] and .dev.vars.example: add APP_VERSION and
  HEALTH_DB_TIMEOUT_MS
- tests/unit/health.test.ts: full mocked coverage matching the template

Also refactors pingDatabase in both the Workers template and the example
to take the binding directly (Hyperdrive | undefined / D1Database |
undefined) instead of the whole Hono Context — the Context<Bindings>
variance caused a typecheck error when the route's wider Bindings
included APP_VERSION/HEALTH_DB_TIMEOUT_MS. Decoupling db/ping.ts from
Hono's type also makes it easier to reuse outside HTTP handlers.

Example tests: 19/19 passing (7 health + 12 todos integration).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@PAMulligan PAMulligan linked an issue May 14, 2026 that may be closed by this pull request
4 tasks
@github-actions github-actions Bot added area: scripts Automation scripts area: templates Starter templates area: docs Documentation labels May 14, 2026
@PAMulligan PAMulligan self-assigned this May 14, 2026
@PAMulligan PAMulligan merged commit 331e26d into main May 14, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: docs Documentation area: scripts Automation scripts area: templates Starter templates

Projects

Development

Successfully merging this pull request may close these issues.

Add health check endpoint with dependency status

1 participant