Express 5 + TS backend service template
Find a file Use this template
2026-11-05 11:07:19 -06:00
.forgejo fix: derive app-name at runtime; template expansion breaks ${{ }} expressions 2026-11-05 01:57:51 -06:00
migrations feat: initial backend service template 2026-11-05 01:43:18 -06:00
src feat: Unleash feature flags via src/lib/flags.ts, hello-banner on GET / 2026-11-05 11:07:19 -06:00
.dockerignore feat: initial backend service template 2026-11-05 01:43:18 -06:00
.env.example feat: Unleash feature flags via src/lib/flags.ts, hello-banner on GET / 2026-11-05 11:07:19 -06:00
.gitignore feat: initial backend service template 2026-11-05 01:43:18 -06:00
.nvmrc feat: initial backend service template 2026-11-05 01:43:18 -06:00
.prettierignore feat: initial backend service template 2026-11-05 01:43:18 -06:00
.prettierrc.json feat: initial backend service template 2026-11-05 01:43:18 -06:00
CLAUDE.md feat: Unleash feature flags via src/lib/flags.ts, hello-banner on GET / 2026-11-05 11:07:19 -06:00
commitlint.config.js feat: initial backend service template 2026-11-05 01:43:18 -06:00
Dockerfile feat: initial backend service template 2026-11-05 01:43:18 -06:00
eslint.config.js feat: initial backend service template 2026-11-05 01:43:18 -06:00
package-lock.json feat: Unleash feature flags via src/lib/flags.ts, hello-banner on GET / 2026-11-05 11:07:19 -06:00
package.json feat: Unleash feature flags via src/lib/flags.ts, hello-banner on GET / 2026-11-05 11:07:19 -06:00
README.md feat: Unleash feature flags via src/lib/flags.ts, hello-banner on GET / 2026-11-05 11:07:19 -06:00
tsconfig.json feat: initial backend service template 2026-11-05 01:43:18 -06:00
vitest.config.ts feat: initial backend service template 2026-11-05 01:43:18 -06:00

${REPO_NAME}

Express 5 + TypeScript backend generated from nholtware/service-template-express.

Endpoints

  • GET /healthz liveness, always 200 once listening
  • GET /readyz readiness: Postgres SELECT 1 and Redis PING for whichever of DATABASE_URL / REDIS_URL are set; 503 + JSON reason otherwise
  • GET / service name, version (git sha), pod, a service_info row, Redis hit counter, and banner while the hello-banner flag is on
  • GET /log-demo?msg=... logs { demo: true, text } with message log-demo (logging-pipeline demo; delete when you don't need it)

Local dev

cp .env.example .env   # then export the vars, or use your shell's env loader
npm ci && npm run dev
npm run lint && npm test

Env is validated at boot (src/lib/config.ts): PORT, SERVICE_NAME (required), LOG_LEVEL, DATABASE_URL?, REDIS_URL?, UNLEASH_URL?, UNLEASH_API_TOKEN? (required with UNLEASH_URL), UNLEASH_REFRESH_MS (default 5000). Migrations: SQL files in migrations/, applied with npm run migrate. Logs: one JSON object per line on stdout (level,time,msg,service,version, plus req.*/res.statusCode/responseTime per request).

Seeing your logs

Every container's stdout is shipped to Elasticsearch by Fluent Bit; nothing to configure per service.

  1. Open Kibana: http://kibana.192.168.1.83.nip.io, user elastic, password from kubectl get secret -n logging logging-es-elastic-user -o jsonpath='{.data.elastic}' | base64 -d.
  2. Discover, data view kube-* (the default).
  3. Query with KQL, e.g. service:"${REPO_NAME}" and demo:true, or service:"${REPO_NAME}" and level:"error".

Any field you pass to the logger becomes a queryable field: req.log.info({ orderId }, "order placed") → KQL orderId:123. The message string always overrides a msg key in the object, so don't name a field msg. Lines are searchable within a few seconds; retention is 14 days.

Feature flags

Flags come from Unleash (UI: http://unleash.192.168.1.83.nip.io, environment development). src/lib/flags.ts wraps the official unleash-client SDK; routes get it as deps.flags:

if (deps.flags?.isEnabled("my-flag", { userId })) { ... }
const v = deps.flags?.getVariant("my-experiment"); // v.name, v.enabled, v.payload
  • In the cluster the deployment sets UNLEASH_URL=http://unleash.unleash.svc:4242/api and UNLEASH_API_TOKEN from Secret unleash-client-token (key token) in the service's namespace.
  • The SDK polls every 5 s; flipping a flag in the UI changes behaviour within seconds, no redeploy.
  • With UNLEASH_URL unset (local dev, tests) every flag reads false and one log line says so. If Unleash is down, flags keep their last fetched value (or false) and the service keeps serving.
  • Create the flag in the Unleash UI (project default) before using it; unknown flags read false.
  • Tests: pass a fake Flags to createApp (see src/app.test.ts); never hit a real Unleash.
  • hello-banner on GET / is the platform demo flag; delete it when you don't need it.

Using this template

  1. Generate a repo from this template in Forgejo (public). The repo name is substituted into package.json, this README, .env.example, and the first migration (CI derives the app name from the repo name itself).
  2. Set 3 Actions secrets on the new repo: REGISTRY_USER (nholtware), REGISTRY_TOKEN (registry push token), GITOPS_TOKEN (ci-bot gitops token).
  3. Add gitops entries: render templates/backend/ (see its README) into manifests/<app>/ and add apps/<app>.yaml in nholtware/gitops.
  4. Push to main. Generating a repo emits no push event, so CI only starts on your first push.