Repository navigation
feat: add API skeleton and its Docker image - #11
Conversation
AbdikhaniAbdirashid
left a comment
There was a problem hiding this comment.
Ran it locally rather than just reading it. Everything you claim holds up.
bun test 15/15, bun run typecheck clean. Probed the running server: /health/live and /version answer, an unknown route returns the contract's error shape, X-Request-Id is on the response and the same id is in the body, broken JSON gives 400 bad_request, a 300 KiB body gives 413 payload_too_large, SIGTERM exits 0 in 58 ms, and the health routes produce zero log lines.
The buildApp() / server.ts split is the part I'll remember — being able to test routes with inject() and no port is why 15 tests run in under 300 ms.
One thing to settle
/health/ready always answers 200. docs/api.md says it should be "200, or 503 while shutting down or when the database is unreachable". There's no flag or onClose hook, so the 503 can't happen.
Nothing breaks today since it isn't deployed, and the preStop sleep in CONTEXT.md covers the same window anyway. But the code and the contract say different things, and I'd rather we pick one now than have someone rely on the wrong one later. Either the route learns to answer 503 while closing, or the contract drops that clause.
Smaller
bun.lock won't parse on my machine — "Unknown lockfile version" on Bun 1.3.7. Tests still ran, so it's on me to upgrade to 1.4.4, but worth knowing that the pin isn't just advisory.
A question
/health/ready is where the database check lands in phase 2. If the database is briefly unreachable, every pod goes unready at once and the whole API disappears rather than degrading. Is that the intent, or should readiness tolerate a short outage?
Approving — the one point above isn't blocking.
|
Thanks for running it rather than just reading it. That's exactly the kind of review that catches things.
It does happen, through Fastify rather than our code: after
Good to know, and it proves the pin is real. In the CI step (1.5) I'll add a
Good question. My suggestion: readiness does check the database (a pod that can't reach it shouldn't get traffic, for example right after startup), but with One more thing: I pushed a second commit after your approval, |
AbdikhaniAbdirashid
left a comment
There was a problem hiding this comment.
Re-approving: my first review was before the Dockerfile landed, so this covers that and the 503 note.
Built and ran the image rather than reading it. 204 MB, runs as uid 1000 (bun), /health/live, /health/ready and /version all answer, and --build-arg GIT_SHA=abc1234 comes back out of /version, so the pipeline's tag will be visible on the running pod. An unknown route returns the contract's error shape. No TypeScript and no *.test.ts in the image - --production plus the .dockerignore do what they say. docker stop exits in 121 ms.
Two things I'd point at for anyone reading this later:
No build step. Bun runs the TypeScript directly, so the image is install + copy src. Mine needs npm run build first because a browser can't run TSX. Good illustration of what the runtime choice actually bought.
Exec-form CMD. CMD ["bun", "src/server.ts"] makes Bun PID 1, so SIGTERM reaches it. Shell form would put /bin/sh in front and swallow the signal, and the graceful shutdown you measured would quietly stop working in the container while still passing on the host. That one is easy to get wrong and hard to notice.
On the 503: the comment answers my question completely - it comes from Fastify after app.close(), not from route code. That was my misreading, and it's now written down in both the code and the contract.
One small thing, not blocking: oven/bun:1.4.4-slim is Debian-based, which is where most of the 204 MB is. If the image size ever matters, oven/bun:1.4.4-alpine is a fraction of it - though musl vs glibc is a real difference and probably not worth the risk this close to the defense.
Step 1.5: the API pipeline in GitHub Actions (`.github/workflows/api.yml`). ## What it does - **Every pull request:** `api-test` installs with the frozen lockfile, then runs `typecheck` and `bun test`. `api-image` then checks that the Docker image builds for `linux/amd64`. Nothing is pushed. - **Every merge to `main` that touches `api/`:** the same tests, then the image is built and **pushed to GHCR** as `ghcr.io/devinder-dev/relay-api:<commit>`. Deployment comes in step 1.8. ## Choices - **No path filter on pull requests:** `api-test` will be a required check, and a required check that never runs (for example on a dashboard-only PR) would block the merge forever. The API tests take about 30 seconds. - **Bun 1.4.4 from `api/.bun-version`:** one file that CI reads and that documents the version for our laptops. - **Actions pinned to full commit SHAs**, with the version as a comment, so a moved or hijacked tag can't run new code. `docker/login-action` is v4.6.0, because v4.7.0 was only 3 days old. - **Least privilege:** `contents: read` by default. Only `api-image` gets `packages: write`, and it only logs in and pushes on `main`. - **Images are built on GitHub's amd64 runners**, which match the VPS. Images built on a Mac would be arm64. - `concurrency` cancels outdated PR runs. The `gha` cache reuses unchanged image layers. ## After the first push to `main` 1. Make the GHCR package `relay-api` public, so k3s can pull it without credentials. 2. Add `api-test` as a required check in the `protect-main` ruleset. Rebased onto `main` after #11 was merged, so it now contains only the workflow and `api/.bun-version`.
The first code of the Relay API, step 1.3 of the walking skeleton: a Fastify app on Bun 1.4.4 with health routes, checked configuration, one error handler and graceful shutdown. It also adds the API's Docker image (step 1.4). Nothing is deployed yet; CI and deployment come in steps 1.5–1.8.
What's in
api/src/server.ts0.0.0.0:PORT, and shuts down gracefully on SIGTERM/SIGINT (app.close(): new requests get 503, running ones finish, exit 0)src/app.tsbuildApp(): 256 KiB body limit, a UUID request id per request (X-Request-Idheader +request_idin every JSON log line), health routes not logged, error handlerssrc/config.tsPORT,GIT_SHA,LOG_LEVELchecked with Zod at startup. A bad value stops the app with a clear messagesrc/routes/health.ts/health/live,/health/ready,/version(docs/api.md)src/errors.tsAppError+notFound()/conflict(), plain TypeScript with no Fastify importsrc/error-handler.tsdocs/api.md: Zod → 422validation_failedwithdetails,AppError→ its status, Fastify 4xx → 400/413/415, anything else → logged + 500internal_errorwith no detailsPinned exactly: Bun 1.4.4 types, Fastify 5.12.7, Zod 4.6.7, TypeScript 6.0.3. Follows decisions 0001 and 0003.
How it was checked
bun test: 15 tests (health routes, request id, config defaults and rejections including the"3000abc"/"80.5"regression, every error-handler branch, and that a crash leaks no details).bun run typecheck: clean.bun audit: no known vulnerabilities in 53 packages.Docker image (
api/Dockerfile)oven/bun:1.4.4-slim(Debian/glibc). Not alpine, because Bun's musl build is the less-tested one; not distroless yet, because a shell helps debugging.--frozen-lockfile --productionin the first, so dev tools and Bun's download cache stay out of the final image.bun(uid 1000).GIT_SHAis passed at build time, so/versionreports the image's own commit.CMD ["bun", "src/server.ts"], so Bun is PID 1 and receives SIGTERM itself..dockerignorekeepsnode_modules, tests and.envfiles out.Checked: the image runs as uid 1000,
/versionshows the build arg, there are no test files in the image, anddocker stoptakes 0.12 s with exit code 0 and the "shutting down" log line.How to run it