Deployment & CI/CD

TypeScript Real World Applications — Deployment & CI/CD
TypeScript Real World Applications
Course 3 · Chapter 9 · Deployment & CI/CD

🚀 Deployment & CI/CD

Every pattern this course covered — dependency injection, tests, typed config, structured logging — exists to make an app safe to change. This final chapter closes the loop: getting that app into production automatically, safely, and in a way that fails loudly and rolls back cleanly when something goes wrong.

Containerizing a TypeScript App

A TypeScript app needs to compile before it runs — shipping the TypeScript compiler itself to production is unnecessary weight. A multi-stage Docker build keeps the final image lean:

Single-Stage vs Multi-Stage Build

Single-Stage
FROM node:20 WORKDIR /app COPY . . RUN npm install RUN npm run build CMD ["node", "dist/main.js"]

Ships node_modules, dev dependencies, TypeScript source, and the compiler — a bloated image with a larger attack surface.

Multi-Stage
FROM node:20 AS build WORKDIR /app COPY . . RUN npm ci && npm run build FROM node:20-slim WORKDIR /app COPY --from=build /app/dist ./dist COPY --from=build /app/node_modules ./node_modules COPY package.json . CMD ["node", "dist/main.js"]

Only the compiled JavaScript and production dependencies make it into the final image.

🔁 Automated Testing in Pipelines

The CI pipeline is where every earlier chapter's safety net actually gets enforced — a pull request can't merge unless the type checker, the linter, and the test suite (Chapter 2) all pass:

A GitHub Actions Pipeline

# .github/workflows/ci.yml name: CI on: [pull_request] jobs: verify: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 20 } - run: npm ci - run: npm run typecheck # tsc --noEmit - run: npm run lint - run: npm test # the Jest suite from Chapter 2 - run: npm run build # fails the build if compilation fails

tsc --noEmit

Runs the type checker without producing output files — a fast pass/fail signal, separate from the actual build step.

Fail the Build, Not the Deploy

Every check above runs before a deployable artifact even exists — a broken build never reaches a production server.

🐤 Canary Deployments

Even a build that passes every check can behave badly under real production traffic. A canary deployment sends a small percentage of traffic to the new version first, before rolling it out fully:

// A simple traffic-split decision, typed so a typo can't silently misroute everyone type DeploymentTarget = "stable" | "canary"; function routeRequest(canaryPercent: number): DeploymentTarget { return Math.random() * 100 < canaryPercent ? "canary" : "stable"; } // Start small — 5% of traffic — and watch the metrics/logs from Chapter 5 // before increasing canaryPercent toward 100.

Gradual Rollout

5% → 25% → 50% → 100%, watching error rates and latency metrics (Chapter 5) at each step before increasing further.

Feature Flags

A discriminated union of flag states (like Chapter 3's API versions) lets new code paths ship dark and be enabled independently of the deploy itself.

Rollback Strategies

Every deployment should assume it might need to be undone quickly — Chapter 4's fail-fast config validation is exactly what makes this safe: a bad deploy fails its own health check immediately instead of serving broken traffic silently.

// A startup health check that reuses Chapter 4's config validation directly async function healthCheck(): Promise<boolean> { try { loadConfig(); // throws (Chapter 4) if config is invalid await database.ping(); // confirms the DB connection actually works return true; } catch { return false; } } // The deployment platform polls this before routing any real traffic to the // new version — a failing health check triggers an automatic rollback to // the previous, known-good build artifact.

Keep the Previous Artifact

A rollback should mean "redeploy the last known-good build," not "revert the commit and rebuild from scratch" — the former is seconds, the latter is minutes.

Automated, Not Manual

A health check failure should trigger a rollback automatically — waiting for a human to notice a production incident defeats the purpose of having one.

💻 Coding Challenges

Challenge 1: Write a Multi-Stage Dockerfile

Write a two-stage Dockerfile for a TypeScript project: a build stage that runs npm ci && npm run build, and a slim final stage that only copies the compiled output and production dependencies.

Goal: Practice keeping the compiler and dev dependencies out of the final production image.

→ Solution

Challenge 2: Build a CI Pipeline

Write a GitHub Actions workflow that runs on every pull request: install dependencies, type-check, run the Jest suite, and only then build — with each step depending on the previous one succeeding.

Goal: Practice making every safety net from earlier chapters (types, tests) an actual merge gate, not just a local habit.

→ Solution

Challenge 3: Write a Health Check With Rollback Logic

Write a healthCheck() function that validates config (Chapter 4) and pings a dependency, plus a small deployment script that rolls back to a previous artifact if the health check fails after deploying a new one.

Goal: Practice tying startup validation directly to an automated rollback decision.

→ Solution

⚠️ Gotcha: A Health Check That Always Returns True

A health check endpoint that just returns { status: "ok" } unconditionally provides zero real signal — it will happily report healthy while the database is unreachable or config failed to load. Make the health check actually exercise the dependencies that matter (config validation, a database ping, a downstream service check), the same way Chapter 4's loadConfig() fails loudly instead of silently — a rollback system is only as good as the check that triggers it.

🎓 Course Complete

That closes TypeScript Real World Applications. Across nine chapters, one thread ran through everything: define a type once, derive everything else from it, and validate at every boundary where a type's promise meets the real world — a request body, an environment variable, a database row, a WebSocket message, a deploy. Dependency injection, testing, config, logging, data, real-time messaging, performance, and deployment aren't separate skills; they're the same discipline applied at every layer of a running application. Courses 4 (Advanced TypeScript) and 5 (Building a Production TypeScript Application) are sketched and ready whenever you want to go further.