DEV Community

ANKUSH CHOUDHARY JOHAL
ANKUSH CHOUDHARY JOHAL

Posted on • Originally published at johal.in

How We Refactored Our API from Express 4 to NestJS 11 and Cut Bugs by 30%

How We Refactored Our API from Express 4 to NestJS 11 and Cut Bugs by 30%

For three years, our core REST API ran on Express 4. It powered 12 downstream services, handled 40k requests per minute, and served 2M+ monthly active users. But as our team grew from 4 to 14 backend engineers, Express’s unopinionated structure started to hurt: inconsistent error handling, scattered middleware, no built-in dependency injection, and a 22% monthly bug rate tied to routing conflicts and untested edge cases.

After 6 months of planning and incremental migration, we fully moved to NestJS 11. The result? A 30% reduction in production bugs, 40% faster onboarding for new engineers, and a 25% drop in time spent on API maintenance. Here’s how we did it.

Why We Outgrew Express 4

Express 4 served us well early on. Its minimalist design let us ship fast, but as the codebase grew to 18k lines of JavaScript, we hit three critical pain points:

  • Inconsistent architecture: Each engineer structured routes, controllers, and middleware differently. We had 7 different patterns for error handling across 42 endpoints, leading to 14% of bugs from unhandled rejections.
  • No native dependency injection (DI): We relied on manual module imports and global singletons, which made unit testing a nightmare. 60% of our test suite was integration tests, with slow runtimes (12+ minutes per full run).
  • Middleware sprawl: 28 custom middleware functions were registered globally, causing unexpected side effects for 19% of new endpoints. We once had a auth middleware conflict that took 3 days to debug.

Why NestJS 11?

We evaluated Fastify, Koa, and NestJS. NestJS 11 stood out for three reasons:

  • Opinionated, modular structure: NestJS enforces a standard controller-service-module pattern, which eliminated architectural inconsistency. Its built-in DI system also made testing 3x faster.
  • TypeScript-first design: We’d already migrated 70% of our Express codebase to TypeScript, but NestJS’s decorators and metadata support caught 40% more type errors at build time.
  • Compatibility with existing tooling: NestJS 11 supports Express under the hood, so we could reuse our existing middleware, validation libraries, and OpenAPI specs during incremental migration.

Our Refactoring Process

We avoided a full rewrite (too risky for a production API) and instead used a strangler fig pattern to migrate incrementally over 6 months:

  1. Phase 1: Setup and shared utilities (Month 1): We set up a NestJS monorepo, ported shared utilities (logging, config, error classes) to NestJS modules, and configured the DI container to work with our existing Express middleware.
  2. Phase 2: Migrate low-risk endpoints (Months 2-3): We moved 12 read-only endpoints (health checks, public user profiles) to NestJS first. We used a reverse proxy to route traffic to either Express or NestJS based on path, with no downtime for users.
  3. Phase 3: Migrate core business logic (Months 4-5): We moved 24 write endpoints (user registration, payment processing) to NestJS, rewriting services to use DI and adding unit tests for all critical paths. We kept 100% backward compatibility for downstream services.
  4. Phase 4: Decommission Express (Month 6): We migrated the final 6 legacy endpoints, removed all Express dependencies, and updated our CI/CD pipeline to build and deploy NestJS only.

Challenges We Faced

The migration wasn’t without hurdles:

  • Middleware compatibility: Some Express middleware relied on mutating the req object in ways that didn’t play nice with NestJS’s request lifecycle. We had to wrap 8 middleware functions in NestJS-compatible decorators.
  • Testing gaps: Our Express test suite had 45% coverage. We used the migration as an opportunity to boost coverage to 82% with NestJS’s built-in testing utilities, which added 2 weeks to the timeline.
  • Team training: 6 engineers had never used NestJS before. We ran 3 internal workshops and paired junior engineers with senior NestJS users for the first 2 months of migration.

The Results: 30% Fewer Bugs

We tracked bug rates for 3 months post-migration, comparing to the same 3-month period the year prior:

  • Total production bugs dropped from 47 to 33 (30% reduction).
  • Bugs tied to routing/architecture inconsistencies dropped from 19 to 2 (89% reduction).
  • Time to debug and fix API bugs dropped from 4.2 hours to 1.8 hours on average.
  • Test suite runtime dropped from 12 minutes to 3.5 minutes, with 82% coverage (up from 45%).

We also saw secondary benefits: new engineer onboarding time dropped from 3 weeks to 1.8 weeks, and we were able to add 8 new endpoints in the 2 months post-migration, compared to 5 in the same period pre-migration.

Lessons Learned

If you’re planning a similar migration, here’s what we’d do differently:

  • Don’t skip incremental migration. A full rewrite would have taken 9+ months and risked major downtime.
  • Prioritize testing early. We should have boosted Express test coverage before starting migration to avoid rework.
  • Use NestJS’s built-in OpenAPI tools. We manually updated our API docs post-migration, but NestJS’s Swagger plugin would have automated this.

Conclusion

Moving from Express 4 to NestJS 11 wasn’t just a technical upgrade—it was a cultural shift toward more consistent, testable code. The 30% bug reduction alone paid for the migration effort in 4 months, and we’re now better positioned to scale our API as our user base grows. If your Express codebase is starting to show its age, NestJS 11 is a worthy successor.

Top comments (0)