5 KiB
${REPO_NAME}
Express 5 + TypeScript backend generated from nholtware/service-template-express.
Endpoints
GET /healthzliveness, always 200 once listeningGET /readyzreadiness: PostgresSELECT 1and RedisPINGfor whichever ofDATABASE_URL/REDIS_URLare set; 503 + JSON reason otherwiseGET /service name, version (git sha), pod,greeting(runtime config), aservice_inforow, Redis hit counter, andbannerwhile thehello-bannerflag is onGET /admin/runtime-configcurrent runtime configvalues,loadedAt,source(file|defaults)GET /log-demo?msg=...logs{ demo: true, text }with messagelog-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), RUNTIME_CONFIG_PATH?.
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.
- Open Kibana: http://kibana.192.168.1.83.nip.io, user
elastic, password fromkubectl get secret -n logging logging-es-elastic-user -o jsonpath='{.data.elastic}' | base64 -d. - Discover, data view
kube-*(the default). - Query with KQL, e.g.
service:"${REPO_NAME}" and demo:true, orservice:"${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/apiandUNLEASH_API_TOKENfrom Secretunleash-client-token(keytoken) 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_URLunset (local dev, tests) every flag readsfalseand one log line says so. If Unleash is down, flags keep their last fetched value (orfalse) and the service keeps serving. - Create the flag in the Unleash UI (project
default) before using it; unknown flags readfalse. - Tests: pass a fake
FlagstocreateApp(seesrc/app.test.ts); never hit a real Unleash. hello-banneronGET /is the platform demo flag; delete it when you don't need it.
Runtime config
Typed values you change with a git commit, live in running pods without a restart (timeouts, limits, copy; not on/off-for-whom, that's a feature flag; never secrets).
- Schema + defaults:
src/runtime-config.schema.ts(zod). Every key needs a default. - Read it per request:
deps.runtimeConfig.get().maxItems. React to changes withruntimeConfig.onChange((values, changedKeys) => ...). - In the cluster: ConfigMap
<app>-runtime-config(keyruntime.yaml) in gitopsmanifests/<app>/runtime-config.yaml, mounted as a directory at/etc/app/,RUNTIME_CONFIG_PATH=/etc/app/runtime.yaml. Edit, commit, push: live within ~4 min (ArgoCD polls every 3 min, kubelet refreshes the volume within ~1 min). Log lineruntime config reloadedlistschangedKeys. - A bad value (invalid YAML, fails the schema) keeps the last good values and logs
errorwith the zod issues. The pod never crashes on a config commit. Fix it with another commit. - Local dev:
RUNTIME_CONFIG_PATH=./runtime.yaml npm run dev; edits reload immediately. Unset = schema defaults (source: "defaults"). GET /admin/runtime-configis unauthenticated, which is why nothing secret goes in here.
Using this template
- 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). - Set 3 Actions secrets on the new repo:
REGISTRY_USER(nholtware),REGISTRY_TOKEN(registry push token),GITOPS_TOKEN(ci-bot gitops token). - Add gitops entries: render
templates/backend/(see its README) intomanifests/<app>/and addapps/<app>.yamlinnholtware/gitops. - Push to
main. Generating a repo emits no push event, so CI only starts on your first push.