Cloud-Native Enterprise Node.js — Part 1
Cloud-Native Enterprise Node.js — Part 1
(Part 1 of a new article series)
This post is the first entry in a series of articles that will walk you through building, testing, and deploying a truly cloud‑native Node.js application. The base of the series is the Cloud‑native Enterprise Node.js – Revamped project, which is a TypeScript‑first, ESM‑only, Fastify‑based micro‑service skeleton that already ships with a modern tool chain (tsdown, c8, direnv, etc.).
In this first part we’ll:
- Explain the motivation for the project and its “revamped” features.
- Walk through the project setup – from Git init to the first curl.
- Dive into the 12‑Factor App principles that shape the architecture.
- Show you the build & test workflow using tsdown and c8.
- Give a quick overview of the folder layout and the next steps you’ll encounter.
1. Why a Revamped Starter?
The original “cloud‑native‑enterprise‑nodejs” repo was a great introduction to building modern Node.js services, but it left out some of the latest best‑practice tools. The revamped version brings:
| Feature | Old | New |
|---|---|---|
| Language | JavaScript (CommonJS) | TypeScript (ESM) |
| Bundler | esbuild | tsdown (powered by rolldown & oxc) |
| Coverage | nyc | c8 (V8 native) |
| Logging | console.log | pino (optionally pino‑pretty) |
| Environment | .env only | direnv + .envrc + dotenvx |
| Testing | jest | tsx + built‑in test runner |
| Build | npm scripts only | Makefile (future) |
| Deployment | Manual | Docker + k3s (planned) |
The idea is to give you a single, opinionated skeleton that’s ready for production – just add your business logic, pull in your database, and you’re good to ship.
2. Quick Start – From Zero to “Hello”
Below is a shortened version of the README’s bootstrap sequence, but with an explanation of why each step matters.
2.1 Initialise a repo
git initKeeps your history version‑controlled. The project will be published on GitHub, so we’ll commit from the start.
2.2 Pick a Node version
echo "nodejs 24.8.0" >> .tool-versions
asdf(ornvm) will read this file and automatically install the exact Node release that the project expects.
2.3 Create the package
npm init \ --init-author-name "Marco Lehmann" \ --init-author-url "https://m99.io" \ --init-license "MIT" \ --init-version "1.0.0" \ --yesFast‑track the
package.jsonwith sensible defaults.
2.4 Force ESM
npm pkg set type=moduleModern Node.js apps are increasingly ESM‑only; it eliminates the CommonJS/ESM interop headache.
2.5 Add direnv support
brew install direnvtouch .envrcecho "dotenv" >> .envrc
direnvloads environment variables automatically when youcdinto the project. Thedotenvhook lets us use the classic.envfile syntax as well.
2.6 Install Fastify & friends
npm install --save \ fastify@5.5.0 \ fastify-type-provider-zod@6.0.0 \ zod@4.1.8
npm install --save-dev \ typescript@5.9.2 \ @types/node@24.3.0
echo "node_modules" >> .gitignore
fastifyis the fastest HTTP framework in Node.js.fastify-type-provider-zodgives us schema‑driven request validation with Zod. TypeScript 5.9 is the newest LTS, and the@types/nodedefinitions keep the compiler happy.
2.7 Build & Run
Add a handful of npm scripts that use tsdown to bundle, c8 for coverage, and tsx for tests.
# Install the build‑time toolsnpm install --save-dev \ tsdown@0.15.0 \ concurrently@9.2.1 \ c8@10.1.3 \ tsx@3.14.0 \ @dotenvx/dotenvx@1.49.0
# Add scripts (quick copy‑paste)npm pkg set scripts.clean="rm -rf ./dist"npm pkg set scripts.build="tsdown --sourcemap --minify"npm pkg set scripts.build:watch="tsdown --sourcemap --minify --watch ./src"npm pkg set scripts.start:watch="node --watch-path=./dist ./dist/server.js"npm pkg set scripts.start:debug="node --inspect ./dist/server.js"npm pkg set scripts.start="node dist/server.js"npm pkg set scripts.dev="npm run build && concurrently -r \"npm run build:watch\" \"npm run start:watch\""
npm pkg set scripts.test="NODE_V8_COVERAGE=./coverage c8 -r html npx tsx --test --experimental-test-coverage src/**/*.test.ts"npm pkg set scripts.test:watch="npx tsx --watch --test src/**/*.test.ts"
echo "dist" >> .gitignoreecho "coverage" >> .gitignoreWhy tsdown?
tsdown is a thin wrapper around rolldown (the fast bundler) and oxc (for .d.ts), giving you an ESM‑only build with source maps and minification, all without Webpack’s complexity.
Why c8?
c8 leverages V8’s native coverage tooling, producing accurate .json and HTML reports.
Why concurrently?
Run the build watcher and the dev server in parallel; fast feedback loop.
2.8 Bootstrap the Fastify server
Create src/server.ts with a minimal health‑check and a couple of demo routes:
import Fastify from "fastify";import { TypeSchema } from "fastify-type-provider-zod";import { z } from "zod";
const app = Fastify();
app.get("/health", async () => ({ status: "ok" }));
app.get( "/headers", { schema: TypeSchema(z.object({})), }, async (request, reply) => { return { headers: request.headers }; });
app.get( "/codes", { schema: TypeSchema( z.object({ code: z.coerce.number().int().min(200).max(599) }) ), }, async (request, reply) => { reply.code(request.query.code); return { status: "ok" }; });
const start = async () => { try { await app.listen({ port: 3000, host: "0.0.0.0" }); console.log("🚀 Server listening on http://localhost:3000"); } catch (err) { console.error(err); process.exit(1); }};start();Run the dev server:
npm run dev2.9 Test the endpoints
curl -i "http://localhost:3000/health"curl -i "http://localhost:3000/headers" \ -H "Authorization: Bearer my-secret-token-that-will-not-get-logged" \ -H "X-Will-Get-Logged: This header will still get logged"curl -i "http://localhost:3000/codes?code=304"You should see JSON responses and appropriate HTTP status codes.
3. The 12‑Factor App in Action
| Factor | How this repo implements it |
|---|---|
| Configuration | Uses .envrc + dotenvx – env vars are injected automatically. |
| Backing Services | Any service (DB, cache, message broker) is reached via env vars; no hard‑coded URLs. |
| Build / Release / Run | npm run build produces a production‑ready bundle; npm run start is the run stage. |
| Processes | Fastify workers are stateless; persistence goes to external services. |
| Port Binding | app.listen({ port: 3000 }) – binds to an external port. |
| Disposability | npm run start can be started/stopped at will; graceful shutdown via SIGTERM. |
| Dev/Prod Parity | Same build scripts, same TypeScript config – just different env vars. |
| Logs | Currently console.log; next step will switch to pino with optional pretty printing. |
The next part of this series will demonstrate a real Kubernetes deployment, Grafana observability stack, and the NestJS + Fastify bonus.
4. Project Structure (Sketch)
.├─ src/│ ├─ server.ts # Fastify entry point│ ├─ routes/│ ├─ services/│ ├─ config/│ └─ …├─ tests/│ └─ server.test.ts├─ Dockerfile├─ .env├─ .envrc├─ .gitignore├─ Makefile # to be added└─ README.mdPotential TODO list (for the next article):
- Reorganise the directory structure.
- Add pino for structured logging, with a
pino-prettyoption for dev. - Write a Makefile that wraps
npm run clean,npm run build,npm run start,npm run test,npm run docker-build,npm run docker-run, etc. - Build a multi‑stage Docker image (builder → production).
- Deploy to a local k3s cluster and attach the Grafana + Loki + Tempo stack.
- Maybe show how NestJS can be dropped in as a Fastify plugin.
5. Wrap‑Up
You’re now standing on a solid foundation.
- The project compiles TypeScript to a fast ESM bundle.
- It uses modern coverage tools.
- It follows 12‑factor principles.
- It is ready for continuous deployment (CI/CD) once you add the missing parts.
Happy coding! 🚀