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:
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.
Here is the extracted `AuthService.ts` with proper types, parameterized queries, and separated business logic. [Code provided below]
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.
Here is `AuthService.test.ts` covering all requested scenarios using Jest mocks. [Code provided below]
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
- Time spent: 45 minutes (compared to estimated 3-4 hours manually).
- Quality improvement: Added type safety, fixed a critical SQL injection vulnerability, and achieved 100% test coverage on the core business logic.
- Risk mitigation: By explicitly instructing the AI to retain exact HTTP status codes and response formats in the controller, we prevented breaking changes for mobile clients.
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.