Working with legacy codebases can feel overwhelming — a mix of outdated patterns, missing tests, and code that lacks modern structure.
At every company I’ve worked at we’ve had similar challenges. From a 5 year old React codebase when working with Xploro, various legacy SAAS products at Corporation Pop, taking on an agencies codebase when at Chatloop and more in previous companies
The instinct to hit reset and rebuild from scratch is real. But I’ve found that taking small, thoughtful steps forward often works better and delivers real impact without slowing down product development.
Here’s how I’ve tackled improving legacy systems while still shipping features.
Starting small with TypeScript
When I started adding TypeScript to an existing codebase, it was tempting to go all in, but I quickly realized that wasn’t realistic. Starting with a few key modules that we were already working on made things manageable. It wasn’t perfect from day one - there were plenty of any types and @ts-expect-error moments, but each incremental step made the codebase easier to work with.
Some findings:
- Pick your battles: Start with critical modules or parts of the code you’re already working on.
- Stay flexible: You don’t have to type everything perfectly from day one. Tools like @ts-expect-error can help ease the transition as well as generic type casting where needed.
- Build momentum: Over time, as more areas adopt TypeScript, the benefits compound, making future work smoother.
Sneaking in tests
Legacy code without tests is pretty common, especially when adopted from an agency (if you’ve worked agency, you understand that it’s often hard to find the time to fit them in). The trick is to build up test coverage bit by bit without grinding everything to a halt.
- Focussing on high-value areas: Write tests for parts of the app that are business-critical or prone to bugs.
- Test as you go: Add tests while working on new features or fixing bugs.
- Small steps matter: Even a handful of new tests can boost confidence when making changes.
Refactoring along the way
You don’t need a massive refactor to make a difference. Small, targeted improvements can have a big impact.
- Refactor when it makes sense: Clean up messy code while working on features or resolving issues.
- Keep it simple: Focus on making functions clearer and components easier to maintain.
- Automate standards: Use tools like linters to enforce consistent style and catch issues early. I’ve recently come to love Biome
Balancing improvement with product work
The most challenging part? Improving the codebase without slowing down product delivery. Here’s what’s worked for me:
- Align with product goals: Prioritising improvements that help the product, like speeding up performance or making future work easier.
- Show the value: Highlight how better code and tests reduce bugs and speed up development.
- Be realistic: Don’t let improvements block critical product work—balance is key.
The takeaway
Working with legacy code is all about balance. Improving code quality, adding tests, and adopting tools like TypeScript are all worthwhile challenges as long as they don’t derail product goals. Small steps add up, making the codebase cleaner and easier to work with over time, boosting both developer experience and product stability.