Break up a utils file with iterative, clear module boundaries
Break up a utils file without a risky rewrite by extracting small modules step by step, setting clear boundaries, and keeping changes safe to ship.

Why one utils file turns into a problem
A single utils file usually starts with good intentions: put small helpers in one place. Then deadlines hit, the team changes, and anything without an obvious home gets dropped into utils. Over time, “break up a utils file” stops being a nice cleanup task and becomes a blocker for safe changes.
The core problem is ownership. When everything lives together, nothing feels owned. People add “just one more helper” without thinking about naming, dependencies, or whether it belongs to a specific part of the app. Soon, a change that should be simple becomes a guessing game: who uses this, and what will break if I touch it?
Typical symptoms show up quickly:
- Unrelated responsibilities mixed together (formatting, API calls, auth, database, UI tweaks)
- Circular imports because many parts of the app depend on the same file
- “Tiny” functions with hidden side effects (reading env vars, writing to storage, mutating global state)
- Tests that are hard to write because importing one helper pulls in half the app
- Security-sensitive logic scattered across random helpers (token parsing, password rules, secret handling)
The goal isn’t aesthetics. It’s safer changes, easier testing, and clear boundaries so you can update one area without surprising another.
Set expectations early: this is iterative cleanup, not a rewrite. You’re moving code in small steps while keeping behavior the same.
How to tell your utils file has become a catch-all
A utils file becomes a catch-all when it quietly turns into a dependency for everything else. The biggest warning sign isn’t the line count. It’s that one “helper” change can break half the app.
Watch for these signals:
- It mixes unrelated jobs like auth checks, database queries, formatting, and network calls.
- Helpers import app-specific modules (models, routes, controllers) instead of staying generic.
- Functions have side effects but sound harmless from the name.
- You edit utils to ship features that have nothing to do with utilities.
- It creates constant friction: merge conflicts and “why did this break that?” surprises.
Size is only a symptom. Coupling is the real issue. When one file knows about auth rules, the data schema, and the HTTP client, hidden dependencies pile up. Every small change demands wider testing.
Pure helpers like capitalize(), clamp(), or toSlug() are fine to keep as utilities. If a function depends on the database, auth state, or secrets, it’s not a utility. It’s a module waiting to happen.
Pick clear module boundaries before moving code
A module boundary is a simple promise: everything in here is about one kind of job, with predictable inputs and outputs. You don’t need perfect design. You need the next move to be obvious and safe.
Start with categories people already understand. For many apps, these buckets cover most of the mess:
- strings
- dates
- validation
- api
- auth
The most important boundary is between pure helpers and helpers with side effects.
Pure helpers are predictable: same input, same output, no outside effects. They’re easiest to move first because you can test them with simple examples.
Side-effect helpers touch the outside world: environment variables, local storage, cookies, time, randomness, network calls, databases, and global app state. They can also log, mutate objects, or throw in ways other code relies on. Treat them as higher risk and keep them grouped by responsibility (for example, don’t mix auth and API client behavior in the same module).
Keep naming consistent, even if it’s boring. Decide early whether you want folders or flat files, and stick to a simple convention so reviewers can spot “misc” modules before they grow.
Make a quick map of what’s inside (without over-planning)
Before splitting a utils file, get a fast, honest picture of what’s in it. You don’t need perfect documentation. You need enough clarity to move code without guessing.
Start with an inventory of exports. List each exported function and where it’s imported. If a helper is used in 30 places, it needs extra care. If it’s used once, it’s a great early candidate.
A lightweight inventory is usually enough:
- Function name and a one-line description
- Where it’s imported (a few top callers)
- Side effects (env, storage, logging, network)
- Where it should live (date, auth, formatting, db)
- Risk level: low (pure), medium (config), high (stateful or security)
Once you can see the list, look for a first extraction cluster: pure functions that don’t read global state and don’t do I/O. Moving those first gives you quick wins and a clean pattern to repeat.
Step-by-step: extract modules iteratively with low risk
The safest way to break up a utils file is to make progress in small slices that are easy to review and easy to roll back.
Pick one theme (dates, formatting, validation, auth helpers) and treat it as a pilot. You’re aiming for a repeatable routine, not a perfect architecture.
A low-risk extraction loop
- Create a new module file for the theme you picked.
- Move only 1 to 3 closely related functions. If something feels half-related, leave it for later.
- Keep exports stable by temporarily re-exporting the moved functions from the original utils file.
- Update imports in one small area of the app (one page, one feature, one service). Commit.
- Remove the temporary re-exports only after most imports have been migrated.
This keeps each commit small: move a few functions, update a few imports, and stop.
Concrete example
If utils.ts includes formatMoney, parseCurrency, roundToCents, plus unrelated helpers like slugify, sleep, and fetchWithRetry, start with money. Create utils/money.ts, move the money functions, and re-export them from utils.ts so nothing breaks. Then migrate imports in the checkout flow first, and expand from there.
Keep behavior the same while refactoring
The biggest risk isn’t moving code. It’s the “while I’m here” edits that quietly change results. Treat this like moving boxes to a new room: label, carry, place. Don’t redecorate mid-trip.
Start with pure functions. Before moving one, write a tiny test that locks in today’s behavior, even if it’s odd. Known-but-weird beats clean-but-different.
Formatting helpers are a good fit for simple input-output tables. Pick a handful of representative cases, including edge cases like empty values, extra spaces, and non-ASCII characters.
If you don’t have test infrastructure, use quick runtime checks instead of guessing. A temporary assertion or short log around a call site can confirm outputs didn’t change after the move. Remove it once you’re confident.
A common pitfall: “improving” formatPrice(amount) while moving it. If that helper affects invoices or emails, a rounding or symbol change can create mismatched totals or customer confusion. Freeze the output, move it, then schedule improvements as a separate, explicit change.
Handle side effects and security-sensitive helpers carefully
The riskiest parts of a utils file are rarely the string helpers. It’s the functions that touch the outside world: network calls, storage, databases, time, random IDs, and environment variables.
Keep pure helpers separate from side-effect code. Mixing them is how you get bugs like duplicate API calls, missing headers, or data saved in the wrong place.
Put side-effect code into modules with obvious names. For example: auth (token read/write, refresh, logout), api client setup (base URL, retries, headers), storage wrappers, and env access.
To keep callers stable while you move code around, introduce small interfaces. Instead of calling localStorage.getItem('token') everywhere, create a tokenStore with getToken() and setToken(). Later, you can change how tokens are stored without editing half the app.
Treat these helpers as high risk even if they look small:
- Token handling (JWT parsing, refresh, expiry checks)
- Secrets (API keys, service tokens, private URLs)
- Password logic (hashing, comparisons, reset flows)
- Input sanitization and query building
If you’re unsure about a move, keep the old function as a thin wrapper that calls the new module, and remove it only after you’ve shipped safely.
Common traps that turn a refactor into a rewrite
Refactors blow up when changes get too large to reason about. The goal is small extractions you can trust.
Common failure patterns:
- Big-bang moves: moving dozens of helpers at once kills your ability to isolate breakages.
- Mixing cleanup with behavior changes: renaming and logic tweaks in the same commit hide regressions.
- Creating a new dumping ground:
shared/orcommon/too early often becomes utils v2. - Breaking imports with no migration plan: prefer a bridge period with re-exports or aliases.
- Hidden dependencies: helpers that read env vars or global singletons can break due to changed initialization order.
A simple example: formatDate() seems harmless, but it relies on a global locale setting and a timezone env var. After moving it, local tests pass but production runs with a different env var, and now receipts show the wrong day.
Two guardrails help: keep each extraction small enough to review in minutes, and keep behavior identical until the boundary is stable.
Quick checks before you ship each extraction
Treat each extraction like a tiny release.
Make sure the new module has one job and a small surface area. If you can’t describe what it does in one sentence, it still does too much. Also be deliberate about exports. Many helpers were “public by accident” when they lived in one big file.
A short pre-merge checklist:
- Clear purpose: name and exports match one domain
- Intentional exports: only export what callers need
- No circular imports
- No secrets pulled into client bundles
- Build passes without warnings that suggest duplicate paths or dead imports
Then do one small real-usage smoke test. Even with unit tests, it’s worth verifying your core flow still works (auth, the main create/save action, and one page that hits the API and renders real data).
Example: splitting a mixed utils file in a real app
A startup has a single utils.ts that started small, then grew to 900 lines. It holds auth helpers (token storage, session checks), API calls (fetchWithAuth), and UI formatting (dates, currency, display names). Bugs show up in strange places because everything imports everything.
They keep feature work moving by extracting small modules in a safe order:
- Move pure formatting helpers first.
- Extract the API client wrapper next and keep old exports as thin wrappers.
- Split auth into separate token and session modules, isolating anything that touches storage.
- Finish with a cleanup pass: remove re-exports, rename unclear functions, and delete dead helpers you can prove are unused.
What improves quickly is practical: fewer regressions, clearer ownership (auth changes don’t affect formatting), easier security review, and faster onboarding because files match real app concepts.
Next steps: turn the plan into steady progress
The fastest way to break up a utils file is to treat it as a series of small, safe moves.
A simple one-week plan:
- Day 1: Pick one boundary (date/time, strings, money, validation) and move 3 to 5 pure functions. Add quick tests.
- Day 2: Move the next 3 to 5 in that same boundary.
- Day 3: Extract one side-effect module (storage, cookies, fetch wrappers). Keep thin wrappers.
- Day 4: Migrate imports in a handful of files. Add a simple rule to discourage new imports from the old catch-all.
- Day 5: Cleanup: rename modules, add short comments, and document what still lives in the old file.
Stop when the remaining utils file is mostly compatibility wrappers and truly shared glue, not the place where everything ends up.
If you inherited an AI-generated app and utils feels like it owns the whole product, FixMyMess (fixmymess.ai) can help by diagnosing the codebase, isolating risky helpers (auth, secrets, injection-prone query builders), and turning the split into a safe, shippable sequence instead of a rewrite.
FAQ
How do I know my utils file is actually a problem and not just “big”?
If the file mixes unrelated jobs and a small change breaks lots of features, it’s already a catch-all. The real signal is coupling: helpers import app-specific modules, have hidden side effects, or are used everywhere.
Line count matters less than how many parts of the app depend on it.
What module boundaries should I pick before I start moving code?
Aim for simple “one job” buckets that match how people think about the app, like strings, dates, validation, api, and auth. Keep pure helpers (no I/O, no global state) separate from side-effect helpers (storage, env, network, database).
If you can’t describe a module in one sentence, the boundary is still too fuzzy.
What should I move out of utils first?
Start with pure, low-risk helpers that are easy to test and don’t read global state. Also prioritize helpers with few call sites because you can migrate them quickly.
Leave high-risk pieces like auth, token handling, env access, and query building for later once you have a safe extraction pattern.
How do I refactor without accidentally changing behavior?
Don’t change behavior while moving code. Write a tiny test (or a quick runtime check) that locks in current output, even if it’s a bit weird.
Treat the move like relocating boxes: move first, improve later in a separate, obvious change.
What’s the safest step-by-step way to split a utils file?
Create the new module, move 1–3 related functions, and keep the old exports working by re-exporting from the original utils file for a while. Then migrate imports in one small part of the app and commit.
Once most call sites are updated, remove the temporary re-exports and repeat with the next cluster.
How do I avoid circular imports when I extract modules?
Circular imports usually mean the “utils” file isn’t actually utilities; it’s doing real domain work and depending on app internals. Split by responsibility and keep dependencies one-directional.
If needed, create a small lower-level module (pure helpers) that higher-level modules can import, instead of everyone importing everyone.
Which utils functions are the most security-sensitive?
Anything that touches tokens, secrets, passwords, env vars, storage, or query building should be treated as high risk. Move those into clearly named modules (like auth, env, storage, db) instead of leaving them scattered among harmless helpers.
Also make sure secret-reading code can’t end up bundled into client-side code by accident.
Should I wrap things like localStorage, cookies, and env access?
Yes, but do it intentionally. Create a small wrapper like tokenStore.getToken() and tokenStore.setToken() so the rest of the app doesn’t call localStorage or cookies directly.
That makes future changes safer, and it reduces the chance of inconsistent token handling across the app.
How do I migrate imports without breaking everything?
Prefer a short bridge period: keep old imports working via re-exports or thin wrappers while you migrate call sites gradually. Keep each extraction small enough that a reviewer can understand it quickly.
Avoid big-bang moves or mixed commits that rename, move, and change logic all at once.
When should I get help breaking up a messy utils file in an AI-generated app?
When you inherited AI-generated code, a giant utils file often hides broken auth flows, exposed secrets, and unpredictable side effects that make every change risky. If you need the split done quickly without turning it into a rewrite, FixMyMess can run a free code audit and then isolate and repair the risky helpers (auth, API client, storage, query builders) with human-verified fixes.
Most projects can be stabilized in 48–72 hours once the highest-risk helpers are identified and separated cleanly.