Article Case Study

Case Study: Refactoring Legacy Auth

A concrete example of using AI to modernize an undocumented legacy authentication controller. From the initial messy state to a clean, tested, and validated result.

Last reviewed: May 26 2026


1. The Starting Point

We inherited an Express.js codebase with a massive, undocumented authentication controller (`auth.js`). It was over 400 lines long, handled database calls directly, lacked type safety, and mixed business logic with HTTP response handling.

Our goal was to extract the logic into a separate service, add TypeScript typings, and write unit tests without breaking existing integrations.

exports.login = async (req, res) => {
  try {
    let u = req.body.username;
    let p = req.body.password;
    if(!u || !p) return res.status(400).send("missing data");
    
    // Direct DB call in controller
    const user = await db.query(`SELECT * FROM users WHERE email='${u}'`);
    if(!user.length) return res.status(401).send("bad auth");
    
    // Hardcoded bcrypt logic mixed in
    const match = await bcrypt.compare(p, user[0].hash);
    if(!match) return res.status(401).send("bad auth");
    
    const token = jwt.sign({id: user[0].id, role: user[0].role}, process.env.SEC);
    
    // Updating DB directly again
    await db.query(`UPDATE users SET last_login=NOW() WHERE id=${user[0].id}`);
    
    res.json({ token, user: { id: user[0].id, email: user[0].email } });
  } catch(e) {
    console.log(e);
    res.status(500).send("server error");
  }
}

2. The AI Workflow

Instead of asking the AI to "fix this file," we broke the task into a structured workflow:

You

I have a legacy Express controller (`auth.js`). I want to refactor it. Step 1: Analyze this code and extract the business logic into a separate TypeScript service class `AuthService`. Do not change the Express controller yet. Only provide the new `AuthService.ts` and the required types. Use parameterized queries to fix the SQL injection vulnerability.

AI

Here is the extracted `AuthService.ts` with proper types, parameterized queries, and separated business logic. [Code provided below]

You

Step 2: Now, generate a suite of Jest tests for `AuthService.ts` mocking the database and bcrypt calls. Include tests for successful login, invalid password, missing user, and database errors.

AI

Here is `AuthService.test.ts` covering all requested scenarios using Jest mocks. [Code provided below]

You

Step 3: Finally, rewrite the original `auth.js` controller (as `authController.ts`) to use the new `AuthService`. Keep the HTTP status codes exactly the same to avoid breaking clients.


3. The Diff

The AI successfully decoupled the logic. The SQL injection vulnerability was patched by using parameterized queries (`db.query('SELECT...', [u])`).

export class AuthService {
  constructor(private db: Database, private jwtSecret: string) {}

  async login(credentials: LoginCredentials): Promise<AuthResult> {
    const { username, password } = credentials;
    
    const users = await this.db.query(
      'SELECT id, email, role, hash FROM users WHERE email = ?', 
      [username]
    );
    
    if (!users.length) {
      throw new UnauthorizedError('Invalid credentials');
    }
    
    const user = users[0];
    const isMatch = await bcrypt.compare(password, user.hash);
    
    if (!isMatch) {
      throw new UnauthorizedError('Invalid credentials');
    }
    
    const token = jwt.sign({ id: user.id, role: user.role }, this.jwtSecret);
    await this.db.query('UPDATE users SET last_login = NOW() WHERE id = ?', [user.id]);
    
    return {
      token,
      user: { id: user.id, email: user.email }
    };
  }
}
export const login = async (req: Request, res: Response) => {
  try {
    const { username, password } = req.body;
    if (!username || !password) {
      return res.status(400).send("missing data");
    }

    const result = await authService.login({ username, password });
    res.json(result);
  } catch (e) {
    if (e instanceof UnauthorizedError) {
      return res.status(401).send("bad auth");
    }
    console.error(e);
    res.status(500).send("server error");
  }
};

4. Test & Eval

Before deploying, we ran the AI-generated test suite. We also ran our end-to-end API tests against the new controller.

PASS  src/services/AuthService.test.ts
  AuthService
    ✓ should return token and user on successful login (32 ms)
    ✓ should throw UnauthorizedError on invalid password (12 ms)
    ✓ should throw UnauthorizedError if user not found (4 ms)
    ✓ should throw DatabaseError if db query fails (3 ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total

5. Result

The Takeaway

AI excels at refactoring when guided by constraints. Instead of "make this better," breaking the task into extraction, testing, and implementation ensured we stayed in control of the architecture while the AI handled the boilerplate and syntax transformation.