Fastify to NestJS Migration: Zero Downtime with 1,000+ Live Users
- Introduction
- Why I Originally Chose Fastify
- When Fastify Started Hurting Us
- 1\. No Enforced Structure
- 2\. Code Maintainability Collapsed
- 3\. No Built-in Dependency Injection
- 4\. Scaling the Team Was Hard
- Why NestJS Was the Answer
- The Migration Strategy
- Step 1 — Map the Entire Codebase
- Step 2 — Run Both Systems in Parallel
- Step 3 — Migrate Module by Module
- Step 4 — Replace Endpoints Gradually
- Step 5 — Validate Everything
- Challenges I Faced
- 1\. NestJS Learning Curve
- 2\. Refactoring Unstructured Code
- 3\. Doing It Alongside Production Support
- Results After Migration
- Code Quality
- Performance
- Staying on Fastify? The Fastify 5 Migration Guide & Breaking Changes
- Key Takeaways
- Final Thoughts
Not a toy project — a real production app with 1,000+ users. This is a step-by-step breakdown of how I planned and executed a full Fastify to NestJS migration without breaking anything.
Introduction
This isn't a side experiment or a tutorial project.
This is a real production application serving 1,000+ active users, handling live data, real workflows, and continuous feature updates.
At one point I made a critical decision — migrate the entire backend from Fastify to NestJS.
Not because Fastify is bad. But because the project had outgrown its initial architecture.
Here's exactly what happened.
Why I Originally Chose Fastify
When I started the project, Fastify was the right choice.
| What Fastify Did Well | Why It Mattered |
|---|---|
| Extremely fast and lightweight | Low latency from day one |
| Minimal setup | Ship features quickly |
| High performance out of the box | Handled early traffic easily |
| Flexible structure | Move fast without constraints |
For an early-stage product, this was perfect.
When Fastify Started Hurting Us
As the product grew, the flexibility that felt like a feature became a liability.
1. No Enforced Structure
src/
├── routes/
│ ├── users.js ← some logic here
│ ├── auth.js ← some logic here too
│ └── payments.js ← and here
├── helpers/
│ └── utils.js ← everything else dumped here
Every developer wrote code differently. No conventions. No consistency. Onboarding new developers was painful.
2. Code Maintainability Collapsed
As features increased:
- Controllers, services, and business logic got mixed together
- Hard to track where dependencies came from
- Even small changes required checking multiple files
- Refactoring became risky
3. No Built-in Dependency Injection
Managing dependencies manually became messy:
// ❌ Fastify — manual dependency management
const userService = require("../services/userService");
const emailService = require("../services/emailService");
const db = require("../db/connection");
async function createUser(req, reply) {
const user = await userService.create(req.body, db);
await emailService.sendWelcome(user.email);
reply.send(user);
}
No clear ownership. No clean separation. Hard to test.
4. Scaling the Team Was Hard
Fastify doesn't enforce structure — and that became the core problem when the codebase grew beyond one developer.
Why NestJS Was the Answer
NestJS solved every problem above.
// ✅ NestJS — clean, structured, testable
@Injectable()
export class UserService {
constructor(
private prisma: PrismaService,
private emailService: EmailService,
) {}
async create(dto: CreateUserDto) {
const user = await this.prisma.user.create({ data: dto });
await this.emailService.sendWelcome(user.email);
return user;
}
}
- Clear module boundaries
- Built-in dependency injection
- TypeScript-first
- Enforced conventions — every developer writes code the same way
- Easy to test in isolation
The Migration Strategy
The most important decision I made: do not rewrite everything at once.
A full rewrite on a live production app with 1,000+ users is too risky. Instead I followed a phased approach.
Step 1 — Map the Entire Codebase
Before writing a single line of NestJS code, I mapped every:
- Route and endpoint
- Service and business logic function
- Database query
- Third-party integration
This gave me a clear picture of the scope.
Step 2 — Run Both Systems in Parallel
Traffic
↓
Nginx / Load Balancer
↓ ↓
Fastify NestJS
(existing) (new modules)
↓ ↓
Same MySQL Database
Fastify kept running. NestJS handled new modules as they were completed. Same database, no data migration required.
Step 3 — Migrate Module by Module
Migration order — least risky to most critical:
Week 1 → Auth module (login, register, JWT)
Week 1 → User profile module
Week 2 → Core business logic modules
Week 2 → Payment and critical flows
Week 2 → Final cutover — Fastify decommissioned
Step 4 — Replace Endpoints Gradually
For each module:
- Build the NestJS equivalent
- Test in staging with real data
- Deploy alongside Fastify
- Shift traffic to NestJS endpoint
- Monitor logs for 24 hours
- Decommission Fastify route
Step 5 — Validate Everything
- Manual testing on every critical user flow
- Verified data consistency between old and new
- Monitored error rates and response times throughout
Building something like this? Hire me to build your backend — NestJS, Node.js, PostgreSQL & Redis, shipped production-ready.
Challenges I Faced
1. NestJS Learning Curve
Modules, providers, decorators, guards, interceptors — NestJS has a lot of concepts. The first week was mostly learning, not migrating.
2. Refactoring Unstructured Code
Old Fastify code wasn't modular. Splitting it into proper NestJS services meant untangling logic that was never meant to be untangled.
3. Doing It Alongside Production Support
Migration happened in parallel with:
- Active feature development
- Bug fixes from real users
- Deployment support
Time management and clear prioritization were critical.
Results After Migration
Code Quality
| Metric | Before (Fastify) | After (NestJS) |
|---|---|---|
| Avg file size | 300-500 lines | 80-150 lines |
| Onboarding time | 3-4 days | 1 day |
| Test coverage | ~20% | ~65% |
| Deployment confidence | Low | High |
Performance
Response times remained solid throughout. NestJS added no meaningful latency overhead — the performance difference between Fastify and NestJS at this scale was negligible compared to the maintainability gains.
Staying on Fastify? The Fastify 5 Migration Guide & Breaking Changes
Maybe you don't need NestJS — you just need to get off Fastify 4. Fastify v5 (Node.js 20+ only) ships several breaking changes worth planning for:
- Node.js 18 support dropped — v5 requires Node 20 or later.
- Callback-style hooks removed in favor of async/await —
done()callbacks in hooks likeonRequest/preHandlermust becomeasyncfunctions. - Stricter route definitions — duplicate routes and shorthand options that v4 tolerated now throw at startup.
request.routeConfigand friends reorganized — route metadata moved underrequest.routeOptions.- JSON schema handling tightened — invalid or loosely-typed schemas that silently worked in v4 now fail fast; run your schemas through validation before upgrading.
- Plugin ecosystem lag — check each
@fastify/*plugin for a v5-compatible release before upgrading; community plugins are the usual blocker.
The safe path mirrors the strategy in this post: upgrade in a branch, run the full test suite, and canary the v5 build next to v4 before cutting traffic over. And if you're hitting the structural pain described above, upgrading Fastify versions won't fix it — that's when the NestJS move (which supports Fastify as its HTTP adapter) makes sense.
Key Takeaways
If you're considering a similar migration:
- Never rewrite everything at once — too risky on a live system
- Run both systems in parallel — gives you a safe rollback path
- Migrate module by module — start with least critical features
- Use the same database — avoid data migration complexity
- Monitor everything — logs, errors, and response times after each deployment
Final Thoughts
Fastify is excellent for speed and simplicity. For early-stage projects it's hard to beat.
But when your application grows, structure becomes more valuable than flexibility.
Migrating to NestJS was one of the best architectural decisions I made for long-term scalability. The codebase is cleaner, features ship faster, and debugging takes minutes instead of hours.
If you're building something you expect to grow — invest in architecture early. The cost of fixing it later is always higher.
Planning a similar migration or stuck on a specific part? Drop a message — I'd love to help.