feat: app health endpoint succeeds (E00-S02-T03)

This commit is contained in:
implementer
2026-08-29 00:21:34 +00:00
parent 9937268d64
commit 9c049ce6b5
7 changed files with 112 additions and 26 deletions
+8 -7
View File
@@ -2,13 +2,14 @@
# @personal-blog/server — EPPP public server application image.
#
# [E00-S02-T01] baseline: builds the workspace server package with the pinned
# toolchain (Node 24.19.0 + pnpm 11.23.0, frozen lockfile) and runs the compiled
# entrypoint. The server is still a bootstrap placeholder (its module loads and
# exits cleanly); the Fastify 5 application shell that turns it into a serving
# process lands in a later story, and the health gate (T02), health endpoint
# (T03), volume persistence (T04), non-root/read-only hardening and multi-arch
# targets are later E00-S02 tasks — all out of scope here.
# [E00-S02-T01/T02/T03] baseline: builds the workspace server package with the
# pinned toolchain (Node 24.19.0 + pnpm 11.23.0, frozen lockfile) and runs the
# compiled entrypoint. Since T03 the entrypoint is a minimal Node `node:http`
# server answering `GET /health` with `{"status":"ok"}` (HTTP 200) on port
# 3000, so the app container stays up and the health endpoint succeeds. The
# Fastify 5 application shell (and the real HTTP API) lands in a later story;
# volume persistence (T04), non-root/read-only hardening (T05) and multi-arch
# targets remain later E00-S02 tasks — all out of scope here.
#
# Image base: node:24.19.0-bookworm-slim (glibc Debian) per Technology-Stack
# §5.4 — argon2 is a native dependency and musl/Alpine causes native-module
+4 -1
View File
@@ -3,12 +3,15 @@
"version": "0.0.0",
"private": true,
"type": "module",
"description": "EPPP public server application. Bootstrap placeholder — the Fastify 5 application shell lands in a later story.",
"description": "EPPP public server application. Serves the application health endpoint (E00-S02-T03); the Fastify 5 application shell lands in a later story.",
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"start": "node dist/index.js"
},
"devDependencies": {
"@types/node": "24.13.3"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
+65 -4
View File
@@ -1,8 +1,69 @@
/**
* @personal-blog/server — EPPP public server application.
*
* Bootstrap placeholder so apps/ is a real workspace package that compiles
* under tsconfig.base.json. The Fastify 5 application shell (and its real
* exports) lands in a later story.
* [E00-S02-T03] minimal serving process: a small HTTP server built on Node's
* `node:http` (no runtime dependencies yet) that answers the application
* health endpoint. `GET /health` reports a healthy application — HTTP 200 with
* `{"status":"ok"}` — so the Compose stack's `app` service stays up and the
* health endpoint succeeds once the stack is running.
*
* The Fastify 5 application shell (and the real HTTP API) lands in a later
* story; this bootstrap keeps the application health-checkable until then.
*/
export {};
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
/** Port the server listens on; `PORT` overrides the container default (3000). */
const PORT = resolvePort(process.env.PORT);
/** Health payload — reports a healthy application. */
const HEALTH_PAYLOAD = JSON.stringify({ status: 'ok' });
/** Payload for any route that is not the health endpoint. */
const NOT_FOUND_PAYLOAD = JSON.stringify({ error: 'not found' });
/**
* Resolves the listen port from `PORT` (default 3000, matching the Dockerfile
* `EXPOSE 3000` and the compose `:3000` container port). A non-numeric or
* out-of-range override falls back to the default so a bad `PORT` value cannot
* crash the process at startup.
*/
function resolvePort(raw: string | undefined): number {
const port = Number(raw ?? 3000);
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : 3000;
}
/** Writes a JSON response with an explicit content-length. */
function sendJson(res: ServerResponse, statusCode: number, body: string): void {
res.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': Buffer.byteLength(body),
});
res.end(body);
}
/**
* Routes one request. The application only serves the health endpoint at this
* stage; anything else is a 404 so misconfiguration is loud.
*/
function handleRequest(req: IncomingMessage, res: ServerResponse): void {
if (req.method === 'GET' && (req.url ?? '/') === '/health') {
sendJson(res, 200, HEALTH_PAYLOAD);
return;
}
sendJson(res, 404, NOT_FOUND_PAYLOAD);
}
const server = createServer(handleRequest);
server.listen(PORT, () => {
console.log(`@personal-blog/server listening on http://0.0.0.0:${PORT} (health: GET /health)`);
});
// `docker stop` (Compose down) and Ctrl-C send SIGTERM/SIGINT — close the
// server and exit cleanly instead of being killed mid-request.
for (const signal of ['SIGTERM', 'SIGINT'] as const) {
process.on(signal, () => {
server.close(() => process.exit(0));
});
}
+2 -1
View File
@@ -2,7 +2,8 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
"outDir": "dist",
"types": ["node"]
},
"include": ["src"]
}