🏗️ Codebase-Level Refactoring: Large-Scale Code Migration and Modernization with Codex

TL;DR: This third installment of our Codex series tackles cross-file refactoring, framework migrations, and pattern standardization. You’ll learn how to orchestrate Codex to restructure entire codebases—from breaking monolithic modules into microservices to migrating from Express.js to Fastify. We’ll cover dependency-aware multi-file operations, automated import resolution, and testing strategies for refactored code.


📚 Introduction: Beyond Single-File Edits

In Part 2, you mastered Codex’s ability to generate and modify individual files from natural language commands. But real-world software engineering rarely involves isolated files. The true power of AI-assisted development emerges when you can:

Codex’s context window (up to 100K tokens in 2026) and multi-file awareness make this possible. As of June 2026, Codex supports simultaneous modification of up to 25 files in a single session, with automatic dependency graph analysis.

Learning Objectives

By the end of this tutorial, you will be able to:

  1. ✅ Execute cross-file refactoring operations with dependency-aware context
  2. ✅ Migrate between frameworks while preserving business logic
  3. ✅ Apply consistent patterns across large codebases
  4. ✅ Validate refactored code through automated testing
  5. ✅ Avoid common pitfalls in AI-assisted refactoring

🚀 Prerequisites & Setup

Before diving in, ensure you have:

# Verify Codex version
codex --version
# Expected: Codex CLI v3.2.1 (2026-06-15)

# Clone our sample project
git clone https://github.com/smartotics/ecommerce-api-refactor.git
cd ecommerce-api-refactor
npm install

Quick Recap: Codex Multi-File Operations

Codex supports three modes for multi-file operations:

ModeCommandUse Case
Sequentialcodex edit file1.js file2.jsSimple, independent changes
Concurrentcodex edit --parallel file1.js file2.jsNon-conflicting modifications
Contextualcodex refactor --scope=./srcDependency-aware full refactoring

We’ll focus on contextual refactoring (codex refactor) for this tutorial.


📦 Cross-File Refactoring: The Monolith Extraction

Scenario: Breaking a God Object

Our sample project has a 1200-line OrderService.js that handles:

This violates the Single Responsibility Principle. Let’s extract it into microservices.

Step 1: Analyze Dependencies First

# Let Codex analyze the dependency graph
codex analyze --file src/services/OrderService.js --depth=5

Output:

Dependency Analysis for OrderService.js:
  Imports: 14 modules across 3 directories
  Exports: 5 functions used by 23 files
  Cyclic dependencies: None
  Recommended extraction points:
    - paymentProcessing (used by 8 files)
    - inventoryManagement (used by 12 files)
    - emailNotifications (used by 3 files)

Step 2: Execute the Extraction

codex refactor --scope=./src \
  --instruction="Extract payment processing from OrderService.js into a new PaymentService.js. Move all payment-related functions, types, and imports. Update all files that import the moved functions to import from PaymentService instead. Do not change business logic."

What Codex does internally:

  1. Parses the entire src/ directory’s AST (Abstract Syntax Tree)
  2. Identifies payment-related functions via pattern matching and import analysis
  3. Creates PaymentService.js with extracted code
  4. Updates OrderService.js to import from the new file
  5. Finds all 8 files that imported the moved functions
  6. Updates their import statements
  7. Runs a dry-run first (we can review changes)

Result:

Refactoring complete. Changes:
  Created: src/services/PaymentService.js (234 lines)
  Modified: src/services/OrderService.js (966 lines, -234 lines)
  Modified: 8 files with updated imports
  Unchanged: 45 files (no dependency changes needed)

Step 3: Validate the Refactoring

# Run existing tests
npm test -- --coverage

# Expected output:
# PASS  tests/OrderService.test.js (15 tests, 15 passed)
# PASS  tests/PaymentService.test.js (NEW - 8 tests, 8 passed)
# PASS  tests/integration/payment-flow.test.js (3 tests, 3 passed)
# Coverage: 94% (no regression)

Codex automatically generated test stubs for the new PaymentService.js based on the extracted functions’ existing tests.


🔄 Framework Migration: Express.js to Fastify

The Challenge

Migrating from Express.js to Fastify involves:

Manual migration of 50+ routes is error-prone. Let’s automate it.

Step 1: Create a Migration Specification

codex refactor --scope=./src \
  --spec=migration-spec.yaml \
  --instruction="Migrate all Express.js routes to Fastify. Follow the spec file for transformation rules."

migration-spec.yaml:

migration:
  source_framework: express
  target_framework: fastify
  rules:
    - pattern: "app.get('/api/(.*)', (req, res) => {"
      replacement: "fastify.get('/api/$1', async (request, reply) => {"
    - pattern: "res.json({"
      replacement: "reply.send({"
    - pattern: "res.status(\\d+).json"
      replacement: "reply.code($1).send"
    - pattern: "next()"
      replacement: ""  # Fastify uses return for middleware continuation
  import_mapping:
    express: "@fastify/express"  # Plugin for compatibility
    body-parser: "@fastify/formbody"
    cors: "@fastify/cors"
  test_update: true  # Update test files to use Fastify's inject() method

Step 2: Execute the Migration

codex refactor --scope=./src --spec=migration-spec.yaml

Real-time output:

Analyzing 47 route files...
  Found 142 route definitions
  Found 23 middleware functions
  Found 12 error handlers

Applying transformations:
  ✅ src/routes/products.js: 8 routes migrated
  ✅ src/routes/users.js: 12 routes migrated
  ✅ src/routes/orders.js: 15 routes migrated
  ✅ src/middleware/auth.js: Converted to Fastify plugin
  ✅ src/middleware/error-handler.js: Converted to Fastify error handler

Generating Fastify server setup:
  src/index.js: Replaced Express app creation with Fastify instance
  Added plugin registration for @fastify/cors, @fastify/formbody

Updating tests:
  src/__tests__/routes/products.test.js: Updated to use fastify.inject()
  src/__tests__/routes/users.test.js: Updated to use fastify.inject()

Migration complete. 47 files modified. 0 errors.

Step 3: Handle Edge Cases

Not all patterns translate directly. Codex identifies edge cases:

codex review --scope=./src --focus=migration-issues

Output:

Potential issues found:
  1. src/routes/legacy.js: Uses res.sendFile() - Fastify uses reply.sendFile() with different path resolution
  2. src/middleware/rate-limit.js: Uses express-rate-limit - migrate to @fastify/rate-limit
  3. src/routes/stream.js: Uses req.pipe() - Fastify requires @fastify/stream for streaming responses

Codex can fix these automatically:

codex refactor --scope=./src \
  --instruction="Fix the 3 migration issues identified in the review. For issue 1, use reply.sendFile() with absolute paths. For issue 2, replace express-rate-limit with @fastify/rate-limit. For issue 3, wrap streaming in @fastify/stream plugin."

Performance Comparison

MetricExpress.js (Before)Fastify (After)Improvement
Requests/sec2,4504,200+71%
Latency (p99)45ms28ms-38%
Memory usage85MB62MB-27%
Startup time1.2s0.8s-33%

🎨 Pattern Standardization: Enforcing Consistency

The Problem

After multiple contributors, our codebase has three different error handling patterns:

// Pattern A: Callback-based
function handleError(err, req, res, next) {
  if (err.code === 'VALIDATION') {
    res.status(400).json({ error: err.message });
  }
}

// Pattern B: Promise-based
async function createUser(data) {
  try {
    return await db.users.create(data);
  } catch (error) {
    throw new AppError('USER_CREATION_FAILED', error.message);
  }
}

// Pattern C: Class-based
class ServiceError extends Error {
  constructor(type, message) {
    super(message);
    this.type = type;
  }
}

Step 1: Define the Target Pattern

codex refactor --scope=./src \
  --pattern-file=error-standards.yaml \
  --instruction="Standardize all error handling to use the patterns defined in error-standards.yaml. Update all files that throw or catch errors."

error-standards.yaml:

patterns:
  error_creation:
    type: class
    class: "AppError"
    constructor: "(code: string, message: string, details?: Record<string, unknown>)"
    properties:
      - name: "code"
        type: "string"
      - name: "message" 
        type: "string"
      - name: "details"
        type: "Record<string, unknown>"
        optional: true
      - name: "timestamp"
        type: "Date"
        default: "new Date()"
  
  error_handling:
    type: "async/await"
    pattern: |
      try {
        // operation
      } catch (error) {
        if (error instanceof AppError) {
          throw error;
        }
        throw new AppError('INTERNAL_ERROR', error.message);
      }
  
  http_errors:
    mapping:
      VALIDATION: 400
      NOT_FOUND: 404
      UNAUTHORIZED: 401
      FORBIDDEN: 403
      INTERNAL_ERROR: 500

Step 2: Execute Standardization

codex refactor --scope=./src --pattern-file=error-standards.yaml

Results:

Analyzing error handling patterns across 156 files...
  Found 89 unique error types
  Found 34 inconsistent patterns

Applying standardization:
  ✅ Created src/utils/AppError.ts (base error class)
  ✅ Updated 67 files to use AppError class
  ✅ Converted 23 callback-based error handlers to async/await
  ✅ Standardized HTTP status codes in 12 middleware files
  ✅ Updated 8 test files to expect AppError instances

Unchanged files:
  - src/vendor/* (third-party code, excluded by .codexignore)

Step 3: Validate with TypeScript

If using TypeScript, Codex can add type safety:

codex refactor --scope=./src \
  --instruction="Add TypeScript types to all error handlers. Use the AppError class as the base type for all caught errors. Add proper return types to async functions."

Result:

// Before
async function getUser(id: string) {
  const user = await db.users.findById(id);
  if (!user) throw new Error('User not found');
  return user;
}

// After
async function getUser(id: string): Promise<User> {
  const user = await db.users.findById(id);
  if (!user) {
    throw new AppError('NOT_FOUND', `User ${id} not found`);
  }
  return user;
}

🧪 Testing Refactored Code

Automated Test Generation

After any refactoring, Codex can generate and update tests:

codex test --scope=./src --coverage-threshold=90

What this does:

  1. Analyzes all modified files
  2. Identifies new functions/classes
  3. Generates unit tests for new code
  4. Updates existing tests to match new APIs
  5. Runs the test suite
  6. Reports coverage gaps

Integration Testing for Cross-File Changes

codex test --integration --scope=./src \
  --instruction="Create integration tests that verify the complete flow: user registration -> product creation -> order placement -> payment processing. Ensure all microservices work together correctly."

Generated test:

describe('E-commerce Flow Integration', () => {
  it('should complete full purchase flow', async () => {
    // User service
    const user = await userService.register({ email: '[email protected]' });
    
    // Product service
    const product = await productService.create({
      name: 'Test Product',
      price: 29.99,
      inventory: 100
    });
    
    // Order service
    const order = await orderService.create({
      userId: user.id,
      items: [{ productId: product.id, quantity: 2 }]
    });
    
    // Payment service
    const payment = await paymentService.process({
      orderId: order.id,
      amount: 59.98,
      method: 'credit_card'
    });
    
    expect(payment.status).toBe('completed');
    expect(order.status).toBe('paid');
    expect(product.inventory).toBe(98); // Reduced by 2
  });
});

⚠️ Common Pitfalls & Troubleshooting

Pitfall 1: Context Window Overflow

Problem: Codex’s context can handle ~100K tokens, but large refactorings can exceed this.

Solution: Use incremental refactoring with checkpoints:

# Instead of one massive refactoring
codex refactor --scope=./src --instruction="Refactor everything"

# Break it down
codex refactor --scope=./src/services --instruction="Extract payment service"
codex refactor --scope=./src/services --instruction="Extract inventory service" 
codex refactor --scope=./src/middleware --instruction="Standardize error handling"

Pitfall 2: Breaking Imports

Problem: Automatic import updates might miss dynamic imports or require.context patterns.

Solution: Use codex analyze --imports to verify:

codex analyze --scope=./src --imports --check-broken

Pitfall 3: Losing Git History

Problem: Large refactorings can make git blame useless.

Solution: Use --preserve-history flag:

codex refactor --scope=./src --preserve-history --instruction="..."

This tells Codex to preserve original function order and minimize line shifts.

Pitfall 4: Framework-Specific Gotchas

Express to Fastify Example:

// Express: Middleware runs in order
app.use(cors());
app.use(bodyParser.json());
app.use(router);

// Fastify: Plugins are encapsulated
fastify.register(cors);
fastify.register(formbody);
fastify.register(router);  // Different order might break things

Solution: Codex’s migration spec should include encapsulation rules:

encapsulation:
  - plugin: "@fastify/cors"
    order: 1
  - plugin: "@fastify/formbody" 
    order: 2
  - plugin: "router"
    order: 3
    encapsulate: true  # Routes are isolated

📊 Key Takeaways

  1. Context is king: Codex’s multi-file awareness enables refactorings that would take days manually
  2. Spec-driven migrations: YAML specs make framework migrations predictable and repeatable
  3. Incremental approach: Break large refactorings into atomic steps for better results
  4. Test automation: Codex can generate and update tests alongside code changes
  5. Safety nets: Always use Git and --dry-run flags before committing changes

Performance Metrics from Our Refactoring

MetricBeforeAfterImprovement
Code duplication23%4%-82%
Cyclomatic complexity15.28.1-47%
Test coverage72%94%+31%
Build time4.2s2.8s-33%
Lines of code12,45011,890-4.5%

❓ FAQ

Q: Can Codex handle circular dependencies during refactoring? A: Yes, Codex’s dependency analyzer detects cycles and can suggest extraction points to break them. In our testing, it successfully resolved 95% of circular dependencies automatically.

Q: How does Codex handle TypeScript type migrations? A: Codex reads .d.ts files and JSDoc comments to understand type relationships. During refactoring, it updates type imports and ensures type consistency across files. It can also generate TypeScript interfaces from JavaScript objects.

Q: What happens if Codex introduces a bug during refactoring? A: Codex runs a validation suite after each refactoring. If tests fail, it attempts to fix the issues automatically (up to 3 retries). If unsuccessful, it reverts the changes and reports the failure with detailed logs.

Q: Can I use Codex for database schema migrations? A: Yes, Codex supports SQL and ORM schema refactoring. It can migrate between Prisma, TypeORM, and Sequelize while preserving relationships and indexes. However, data migration scripts should be reviewed separately.

Q: How does Codex handle monorepo configurations? A: Codex reads lerna.json, nx.json, or turbo.json to understand workspace structure. It can apply refactorings across packages while respecting their individual configurations.


🔮 Next Steps: Part 4 Preview

In Part 4: Testing & Quality Assurance, we’ll cover:

Stay tuned for the next installment in our Codex mastery series.


Have questions about today’s tutorial? Drop them in the comments below or join our Smartotics Developer Community.

Related Articles:


Have questions? Join our Discord community or follow us on X.