You have just been handed a legacy codebase. Maybe you inherited it from a team that left. Maybe your company acquired it. Maybe it is your own project from three years ago, and past-you made some questionable decisions. Whatever the reason, you need to understand what you are working with before you start changing things.
The worst thing you can do with legacy code is start refactoring immediately. Without a thorough audit, you are navigating in the dark. You will fix symptoms instead of causes, break things you did not know existed, and spend months on changes that should have taken weeks.
This checklist gives you a structured approach to auditing any legacy codebase. For each of the 12 points, you will find what to look for, what constitutes a red flag, and how to address the issue. Use it as a systematic evaluation before you write a single line of new code.
1. Dependency Graph Overview
What to check: Generate a visual map of how modules and files depend on each other. This is your architectural X-ray — it shows the actual structure of the code, regardless of what the documentation claims.
What to look for:
- The overall shape of the dependency graph. A healthy codebase has clear clusters with limited connections between them. An unhealthy one looks like a tangled ball of yarn.
- The direction of dependencies. Dependencies should generally flow in one direction (e.g., controllers → services → repositories). Arrows going backward indicate architectural violations.
- The distribution of connections. A few hub nodes are normal (shared utilities, database clients). Too many hubs suggest that boundaries between modules are weak.
Red flags:
- No discernible structure — everything depends on everything
- A single file with more than 20 incoming or outgoing dependencies
- Multiple files importing directly from the database layer across all modules
How to fix: You do not fix a messy dependency graph all at once. Start by identifying the natural cluster boundaries and documenting them. Then, in future work, enforce those boundaries by preventing new cross-boundary imports. Tools like ReposLens can generate the dependency graph automatically from your repository and track how it evolves over time.
2. Circular Dependencies Count
What to check: Count the number of circular dependency chains in the codebase. A circular dependency exists when module A depends on module B, and module B depends on module A — either directly or through a chain of intermediate modules.
What to look for:
- Direct cycles (A → B → A)
- Indirect cycles (A → B → C → A)
- The length of the longest cycle chain
Red flags:
- Any circular dependencies in core business logic modules
- Cycles involving more than 3 modules (these are nearly impossible to untangle incrementally)
- More than 5 circular dependency chains in total
How to fix: For each cycle, identify the weakest link — the dependency that is easiest to break without major restructuring. Common techniques include:
- Extracting shared logic into a new module that both sides can depend on
- Using dependency injection to invert the dependency direction
- Introducing an event system or message bus for cross-module communication
// Before: Circular dependency
// user-service.ts imports from email-service.ts
// email-service.ts imports from user-service.ts
// After: Break the cycle with an event
// user-service.ts emits 'user:created' event
// email-service.ts listens for 'user:created' event
// No direct dependency between the two
3. God Modules (Files With Too Many Imports)
What to check: Identify files that import from an unusually high number of other modules. These are "god modules" — they know too much, do too much, and are impossibly difficult to modify without side effects.
What to look for:
- Files with more than 10 imports from your own codebase (not external packages)
- Files longer than 500 lines
- Files whose name includes "utils," "helpers," "common," or "shared" that have grown beyond their original purpose
Red flags:
- A single file importing from 15+ modules
- A "utils" file over 1,000 lines
- A service file that handles database queries, email sending, payment processing, and logging
How to fix: God modules need to be split. The approach depends on the type:
For utility god modules (utils.ts, helpers.ts): Group the functions by domain and split into string-utils.ts, date-utils.ts, format-utils.ts, etc. Update imports across the codebase. This is mechanical and low-risk.
For service god modules (a service that does everything): Identify the distinct responsibilities and extract each into its own service. Use the dependency graph to understand which parts of the codebase depend on which functions, and split accordingly.
4. Dead Code Percentage
What to check: Estimate the percentage of code that is no longer used — exported functions that nothing imports, components that are never rendered, routes that are never hit.
What to look for:
- Files with no incoming dependencies (nothing imports them) that are not entry points
- Exported functions or classes that have zero consumers
- Feature flags that are permanently off
- Commented-out blocks of code
Red flags:
- More than 15% dead code
- Entire directories that are unreferenced
- Test files for modules that no longer exist
How to fix: Dead code removal is one of the safest refactoring activities. Start with the obvious wins:
- Delete files with zero incoming dependencies (after verifying they are not entry points, scripts, or dynamically imported)
- Remove commented-out code blocks (they live in git history if you ever need them)
- Remove unused exports identified by your IDE or linting tools
- Remove feature-flagged code where the flag has been permanently off for more than 6 months
5. Test Coverage
What to check: Measure the current test coverage and, more importantly, evaluate the quality of existing tests.
What to look for:
- Overall coverage percentage
- Coverage of critical business logic paths (payments, authentication, data transformations)
- Test file freshness — when were tests last updated relative to the code they test?
- Test quality — do tests actually assert meaningful behavior, or are they snapshot tests that nobody reviews?
Red flags:
- Less than 30% overall coverage
- Zero coverage on payment or authentication logic
- Tests that have been disabled or skipped for more than 3 months
- Test files that import from modules that have been renamed or moved (indicating tests are broken and nobody noticed)
How to fix: Do not aim for 100% coverage on a legacy codebase — that is unrealistic and counterproductive. Instead:
- Write tests for the code you are about to change (the "test before you touch" rule)
- Prioritize coverage on business-critical paths: anything involving money, authentication, or data integrity
- Set a coverage floor for new code (e.g., 80%) and enforce it in CI
- Delete broken or meaningless tests — they provide false confidence
6. Outdated Dependencies
What to check: List all third-party dependencies and their current version relative to the latest available version.
What to look for:
- Major version gaps (e.g., running React 17 when React 19 is out)
- Dependencies with known security vulnerabilities (run
npm auditorpnpm audit) - Dependencies that are no longer maintained (last publish date > 2 years ago)
- Duplicated dependencies (two libraries that do the same thing)
Red flags:
- More than 5 dependencies with known security vulnerabilities
- Framework version more than 2 major versions behind
- Dependencies that have been deprecated with no migration plan
package.jsonwith 50+ direct dependencies
How to fix: Create a prioritized update plan:
- Immediate: Dependencies with critical security vulnerabilities
- Short-term: Dependencies more than 2 major versions behind
- Medium-term: Deprecated dependencies that need replacement
- Ongoing: Keep a regular cadence of dependency updates (monthly is a good rhythm)
7. Build Time
What to check: Measure the full build time from clean state, as well as incremental build times during development.
What to look for:
- Clean build time (from scratch)
- Incremental build time (after a single file change)
- CI build time (full pipeline including tests)
- Any build steps that seem disproportionately slow
Red flags:
- Clean build time over 5 minutes for a project under 100K lines of code
- Incremental build time over 10 seconds
- CI pipeline over 15 minutes
- A single build step that takes more than 50% of total build time
How to fix:
- Profile the build to identify the slowest steps (most bundlers have profiling options)
- Check for unnecessary barrel files (
index.tsthat re-export everything) — these defeat tree-shaking and slow down builds - Verify that TypeScript's
incrementalandtsBuildInfoFileoptions are enabled - Consider splitting the project into workspaces if it has grown beyond monolithic scale
- Replace slow tools with faster alternatives (e.g., Biome instead of ESLint + Prettier)
8. Documentation State
What to check: Evaluate the completeness and accuracy of existing documentation.
What to look for:
- README: does it explain how to set up, run, and deploy the project?
- API documentation: are endpoints documented? Is the documentation auto-generated or manually maintained?
- Architecture documentation: is there any description of the high-level architecture?
- Inline comments: are complex algorithms or business rules explained?
- Environment variables: are all required env vars documented with descriptions?
Red flags:
- README has not been updated in over a year
- API documentation references endpoints that no longer exist
- No
.env.examplefile - Architecture diagrams that do not match the actual code structure
- Comments like
// TODO: fix thisor// temporary hackthat are more than a year old
How to fix: Documentation triage — fix what matters most:
- Update the README with correct setup instructions (this blocks every new contributor)
- Create or update
.env.examplewith all required variables - Generate API documentation from source (OpenAPI/Swagger) rather than maintaining it manually
- Delete outdated architecture diagrams (wrong docs are worse than no docs)
- Address TODO comments: either do the task or remove the comment
9. Error Handling Patterns
What to check: Examine how the codebase handles errors, both expected (validation failures, not-found) and unexpected (network failures, runtime crashes).
What to look for:
- Consistent error handling patterns vs. a mix of approaches
- Generic catch blocks that swallow errors
- Error messages that expose internal details to users
- Error boundary/fallback UI for frontend applications
- Logging of errors with sufficient context for debugging
Red flags:
catch (e) { }— empty catch blocks that silently swallow errorscatch (e) { console.log(e) }— errors logged to console with no alerting- Inconsistent error response formats across API endpoints
- No global error boundary in the frontend
- Error messages containing stack traces, file paths, or database queries visible to users
// Red flag: Silent error swallowing
try {
await processPayment(order);
} catch (e) {
// TODO: handle this
}
// Red flag: Inconsistent error responses
// Endpoint A returns: { error: "Not found" }
// Endpoint B returns: { message: "Resource not found", code: 404 }
// Endpoint C returns: { errors: [{ msg: "not found" }] }
How to fix:
- Define a standard error format for the entire application
- Replace empty catch blocks with proper error handling (log, retry, or propagate)
- Add a global error boundary (React ErrorBoundary, Express error middleware)
- Ensure errors are reported to a monitoring service (Sentry or equivalent)
- Sanitize error messages sent to clients — never expose internal details
10. Security Vulnerabilities
What to check: Audit the codebase for common security issues.
What to look for:
- Hardcoded secrets (API keys, database credentials, tokens)
- SQL injection vulnerabilities (string concatenation in queries)
- Cross-site scripting (XSS) vectors (unsanitized user input rendered as HTML)
- Missing authentication on protected routes
- Missing authorization checks (authenticated but not authorized)
- Sensitive data in logs or error messages
Red flags:
- API keys or passwords in source code (even if in config files that are gitignored, they may have been committed previously)
- Raw SQL queries with string interpolation
dangerouslySetInnerHTMLwithout sanitization- Routes in the dashboard that do not check authentication
- Missing CORS configuration or overly permissive CORS (
Access-Control-Allow-Origin: *)
How to fix:
- Run a secrets scanner (
gitleaks,trufflehog) on the full git history - Rotate any exposed credentials immediately
- Replace string-concatenated SQL with parameterized queries or an ORM
- Add authentication middleware to all protected routes
- Implement proper CORS policies
- Set up security headers (CSP, X-Frame-Options, X-Content-Type-Options)
11. Performance Bottlenecks
What to check: Identify areas where the application performs poorly under load or with large data sets.
What to look for:
- N+1 query problems (fetching a list, then querying for each item individually)
- Missing database indexes on frequently queried columns
- Unoptimized images or assets (large images served without compression or resizing)
- Memory leaks (event listeners not removed, subscriptions not cancelled)
- Blocking operations on the main thread (heavy computation in UI code)
Red flags:
- API endpoints that take more than 1 second for common operations
- Database queries without
WHEREclauses on large tables - Frontend bundle size over 1 MB (gzipped) for a standard web app
- No pagination on list endpoints
- Loading entire datasets into memory when only aggregates are needed
How to fix:
- Add database indexes on columns used in
WHERE,JOIN, andORDER BYclauses - Fix N+1 queries by using eager loading or batch queries
- Implement pagination on all list endpoints and list UIs
- Add image optimization (compression, responsive sizes, lazy loading)
- Profile the frontend bundle and code-split large dependencies
12. Architecture Boundary Violations
What to check: Verify that the codebase respects its own architectural boundaries — the intended separation between layers, modules, and domains.
What to look for:
- UI components importing directly from the database layer
- Business logic embedded in UI components or API route handlers
- Cross-domain imports (e.g., the "orders" module importing internal functions from the "users" module instead of going through a public API)
- Configuration and environment variables accessed from arbitrary locations instead of through a centralized config module
Red flags:
- A React component that contains a database query
- API route handlers longer than 50 lines (business logic should be in services)
- More than 10 import paths that cross domain boundaries
- No clear separation between what is "public API" of a module and what is internal
How to fix:
- Document the intended architecture boundaries (which layers exist, what each layer is allowed to import)
- Move business logic from UI components and route handlers into service modules
- Create clear public APIs for each domain module (an
index.tsthat exports only what other modules should use) - Enforce boundaries with linting rules (eslint
import/no-restricted-pathsor equivalent) - Use architecture analysis tools to track boundary violations over time — ReposLens can visualize exactly where cross-boundary imports occur and alert you when new ones are introduced
Summary Table
| # | Check | Green | Yellow | Red | |---|-------|-------|--------|-----| | 1 | Dependency graph | Clear clusters | Some tangling | Everything depends on everything | | 2 | Circular dependencies | 0 | 1-3 | 4+ | | 3 | God modules | None over 10 imports | 1-2 files | 3+ files with 15+ imports | | 4 | Dead code | < 5% | 5-15% | > 15% | | 5 | Test coverage | > 60% on critical paths | 30-60% | < 30% | | 6 | Outdated deps | All within 1 major version | 2-3 major behind | Security vulnerabilities | | 7 | Build time | < 2 min clean | 2-5 min | > 5 min | | 8 | Documentation | Up-to-date README + env | Partial docs | No docs or wrong docs | | 9 | Error handling | Consistent patterns | Mixed approaches | Silent failures | | 10 | Security | No known issues | Minor issues | Hardcoded secrets or SQL injection | | 11 | Performance | Sub-second responses | Some slow endpoints | N+1 queries, no pagination | | 12 | Boundary violations | Clean separation | Some leaks | No boundaries enforced |
How to Prioritize
Not all 12 items carry equal weight. Here is a suggested prioritization:
Fix immediately (blocks all other work):
- Security vulnerabilities (#10)
- Missing or broken documentation for setup (#8)
Fix before any refactoring:
- Dependency graph overview (#1) — you need to see the terrain before you move
- Circular dependencies (#2) — these will undermine any refactoring effort
- Test coverage on code you plan to change (#5)
Fix as part of ongoing work:
- God modules (#3) — split them as you encounter them
- Dead code (#4) — remove it when you find it
- Error handling (#9) — standardize as you touch each module
- Boundary violations (#12) — enforce on new code, fix in existing code gradually
Fix as separate initiatives:
- Outdated dependencies (#6)
- Build time (#7)
- Performance bottlenecks (#11)
Conclusion
Auditing a legacy codebase is not glamorous work, but it is the most valuable thing you can do before refactoring. Every hour spent on this checklist saves days of wasted effort later.
The goal is not to fix everything at once. The goal is to understand what you are working with, identify the highest-risk areas, and create a plan that addresses the most impactful problems first. Print this checklist, score your codebase on each item, and use the results to build a refactoring roadmap that your team can execute with confidence.