Cleaning Preview Code: From Prototype Chaos to Modular Architecture
The goal is not prettier folders. It is recovering the ability to change the system predictably. I prefer incremental boundaries over a large rewrite.
Fast prototyping can make a product look finished long before its architecture is stable. Screens work, authentication exists and real users may already be active, while duplicated components, competing state sources and data calls embedded in UI continue to accumulate.
A production refactor is successful when change becomes predictable again.
1. Map before moving files
I first map critical flows, routes, state owners, data sources, mutations and platform-specific behavior. Refactoring without this map usually relocates complexity rather than reducing it.
type RefactorMap = { criticalFlows: string[]; stateOwners: Record<string, string>; dataSources: string[]; mutationPoints: string[]; riskyModules: string[];};2. Give every state one owner
Server data belongs in a query/cache layer, form drafts in local form state, navigation in the router and durable preferences in persistent storage. Copying the same truth into multiple stores is one of the fastest ways to create prototype-only behavior.
3. Move data access behind a boundary
UI should not need to understand SDK details, authentication semantics and database row shapes. I move those concerns into repository, service or query modules and expose typed domain contracts to the view layer.
export async function getProject(id: string): Promise<Project> { const row = await projectRepository.findById(id); return mapProject(row);}4. Use TypeScript as a contract
API responses, domain entities and view models do not need to share one giant type. Separate contracts make migrations, feature flags and compatibility work far easier to reason about.
5. Prefer strangler refactoring to a rewrite
I replace one boundary at a time, keep adapters around the old path, compare behavior and remove legacy code only after the new path is proven. This keeps a working product available while the architecture changes underneath it.
- Freeze critical behavior with tests or observable checks.
- Start with the module that changes or fails most often.
- Define state and data boundaries.
- Introduce the new module behind an adapter.
- Compare production behavior before deleting the old path.
Definition of done
The refactor is useful when a feature no longer requires understanding five competing state sources, critical mutations are easy to locate and module boundaries can be explained clearly to both humans and coding agents.
From Prototype to Production
Turning a working MVP into a dependable product: production checks, modular refactoring, data migration, audits, rewrite decisions and release gates.