For production, compile TypeScript to JavaScript with tsc and run the output with plain node — faster startup and no dev tooling on the server. Turn on strict compiler checks such as noUncheckedIndexedAccess to catch more bugs.
Configure the app through environment variables (validated at startup, as in the Zod lesson), never hard-coded secrets. Log structured JSON so log platforms can search and alert on it.
Handle SIGTERM: stop accepting new connections, let in-flight requests finish, close database pools, then exit. Hosting platforms and Kubernetes send SIGTERM on every deploy. Package the app in a small multi-stage Docker image that runs as a non-root user.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"noUncheckedIndexedAccess": true,
"sourceMap": true,
"skipLibCheck": true
},
"include": ["src"]
}import { createServer } from "node:http";
const PORT = Number(process.env.PORT ?? 3000);
function log(level: "info" | "error", msg: string, extra: Record<string, unknown> = {}): void {
console.log(JSON.stringify({ time: new Date().toISOString(), level, msg, ...extra }));
}
const server = createServer((req, res) => {
const started = performance.now();
res.on("finish", () => {
log("info", "request", {
method: req.method,
url: req.url,
status: res.statusCode,
ms: Math.round(performance.now() - started),
});
});
if (req.url === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ status: "ok" }));
}
res.writeHead(404).end();
});
server.listen(PORT, () => log("info", "server started", { port: PORT }));
function shutdown(signal: string): void {
log("info", "shutting down", { signal });
server.close((err) => {
if (err) log("error", "close failed", { err: String(err) });
process.exit(err ? 1 : 0);
});
setTimeout(() => process.exit(1), 10_000).unref(); // force-exit if stuck
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npx tsc && npm prune --omit=dev
FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]Key points
- Compile with
tsc, run plainnode dist/main.jsin production. - Validate config at startup; log structured JSON; expose a
/healthendpoint. - Handle
SIGTERMfor zero-downtime deploys; run containers as non-root.
Exercise
Take your task API from the earlier lessons, add a /health endpoint, structured logging and graceful shutdown, build it with the Dockerfile above, run it with docker run -p 3000:3000, and confirm docker stop triggers a clean shutdown in the logs.
Show solution
Try the exercise yourself first — then compare your approach with this one.
Combine the task routes with the production pieces from the lesson: a /health endpoint, one JSON log line per request, and a SIGTERM handler that stops accepting connections and exits once in-flight requests finish. Compile with tsc, run with plain node, and docker stop sends the SIGTERM you can watch in the logs.
import { createServer } from "node:http";
const PORT = Number(process.env.PORT ?? 3000);
const tasks = [{ id: 1, title: "Ship it", done: false }];
function log(level: "info" | "error", msg: string, extra: Record<string, unknown> = {}): void {
console.log(JSON.stringify({ time: new Date().toISOString(), level, msg, ...extra }));
}
const server = createServer((req, res) => {
const started = performance.now();
res.on("finish", () =>
log("info", "request", { method: req.method, url: req.url, status: res.statusCode, ms: Math.round(performance.now() - started) }),
);
res.setHeader("Content-Type", "application/json");
if (req.url === "/health") return res.end(JSON.stringify({ status: "ok", uptime: Math.round(process.uptime()) }));
if (req.method === "GET" && req.url === "/tasks") return res.end(JSON.stringify(tasks));
res.writeHead(404).end(JSON.stringify({ error: "Not found" }));
});
server.listen(PORT, () => log("info", "server started", { port: PORT }));
function shutdown(signal: string): void {
log("info", "shutting down", { signal });
server.close(() => {
log("info", "closed cleanly");
process.exit(0);
});
setTimeout(() => process.exit(1), 10_000).unref();
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));docker build -t task-api .
docker run --name task-api -p 3000:3000 task-api
curl localhost:3000/health # {"status":"ok","uptime":3}
docker stop task-api # logs: "shutting down" (SIGTERM) then "closed cleanly"