Skip to content
Nishant.
Back to work

Case study · 2024

Company-wide Node.js platform migration

Sole technical owner

Sole technical owner of a Node v18 → v22 upgrade across every backend service and microservice, delivered with zero customer-facing downtime.

  • Node.js
  • TypeScript
  • AWS SDK v3
  • Prisma
  • Microservices
  • CI/CD

Context

The platform is a stakeholder-engagement product used by enterprise and government clients — a set of Node.js backend services and microservices sharing databases, build tooling, and deployment pipelines. The runtime had drifted to Node 18, which was approaching end-of-life. Staying put meant losing security patches and falling behind on language and dependency support; several libraries we wanted to adopt already required newer runtimes.

I owned the upgrade end to end — scoping, sequencing, execution, and rollout — across all services.

The problem, precisely

A runtime bump sounds like changing one number in a Dockerfile. In a shared multi-service codebase it isn’t. The real problem was coordinated change under a zero-downtime constraint:

  • Multiple services shared libraries and a common build/tooling layer, so they couldn’t all be upgraded independently, but they also couldn’t all be upgraded at once without unacceptable blast radius.
  • Node 18 → 22 crossed breaking changes in native modules and several transitive dependencies, so “it compiles” was not the same as “it’s safe.”
  • The same effort was the right moment to retire accumulated tooling debt — otherwise we’d pay the migration tax twice.

Constraints I designed around

  1. No customer-facing downtime. Production had to stay stable throughout.
  2. Reversibility. Every step needed a clean rollback path; a big-bang cutover was off the table.
  3. One migration, not many. Bundle the runtime bump with the dependency and tooling upgrades it forced, rather than doing three disruptive passes.

Approach

Sequenced, not simultaneous. I ordered services by risk and coupling — leaf services and internal tooling first to validate the upgrade path, then the higher-traffic core services once the pattern was proven. Each service moved behind its own reviewable change so a regression could be isolated to one rollout, not the whole fleet.

Bundled the forced upgrades. The runtime jump pulled in matching work, which I folded into the same migration instead of deferring:

  • AWS SDK v2 → v3 — moving to the modular clients, which also trimmed cold bundle size.
  • Prisma ORM upgrade — aligning the data layer with the new runtime.
  • Tooling modernization — replacing legacy Gulp tasks with plain npm scripts for builds, schema updates, and automation, removing a brittle layer that new contributors kept tripping over.

Made types do the verification. Alongside the runtime work I pushed strict TypeScript across the services, converting previously loosely-typed code into fully strongly-typed modules. That turned a class of runtime-only migration risks into compile-time errors — the compiler caught breakage the runtime bump would otherwise have surfaced in production.

Phased rollout with a rollback path. Services shipped in planned waves. Each wave was observed in production before the next started, so problems stayed contained and every step could be reverted independently.

Outcome

  • Every backend service and microservice migrated from Node 18 to Node 22, with production stability maintained throughout and no customer-facing downtime.
  • AWS SDK v3, an upgraded Prisma, and an npm-script-based build pipeline landed in the same effort — the migration tax was paid once.
  • The services came out fully strongly-typed, improving reliability and raising day-to-day developer velocity for the whole team.

What I’d carry forward

The leverage was in sequencing and bundling, not in any single code change. Ordering by risk bought a safe place to learn the upgrade path before touching the services that mattered most; bundling the forced upgrades avoided repeating a disruptive migration three times; and leaning on the type system converted “hope it still works” into “the compiler says it does.” The upgrade itself was routine — owning the plan is what kept it boring, which for a production migration is exactly the goal.