Using AI for Code Refactoring With Conventions Document

The problem with “just refactor this”
So around late 2024, I was working on refactoring a TypeScript/NestJS codebase at work. Pretty big codebase, lot of service files that need to be cleaned up. Naturally I was thinking to use AI for helping me speed things up, Claude, ChatGPT, whatever tool that was available at that time.
The first few time, I just pasted the code and said something like “refactor this service to follow clean architecture patterns.” Simple enough, right?
The output was… fine-ish. But here’s the thing, every time I run it, I got different result. One time the functions was named with verb-first pattern, next time they were not. One time it used early returns, next time it nesting everything in if-else blocks. The AI was technically correct each time, but the style was inconsistent with rest of the codebase.
If I wanted to use AI for refactoring across multiple files, which I do, because that’s the whole point, this inconsistency would make the codebase look like five different developer wrote it on five different days. That’s not refactoring, that’s just creating new problem haha.
The conventions document
After some frustration tinkering around with different prompts, I realized the missing piece: an explicit conventions document. Not just telling the AI what to do on each prompt, but giving it a written standard that it should comply with. So I sat down and wrote a full TypeScript/NestJS conventions file covering everything the codebase should follow.
Here is a simplified version of what I put in it:
# TypeScript/NestJS Conventions
## Nomenclature
- PascalCase: classes, interfaces, enums
- camelCase: functions, variables, properties
- kebab-case: file names (user-profile.service.ts)
- Function names start with verb: getUserById, validateInput
- Booleans: isActive, hasPermission, canDelete
- No magic numbers, no abbreviations (except i, err, ctx, req, res)
## Functions
- Max 20 instructions per function
- Single purpose, single level of abstraction
- Early returns over deep nesting
- Multiple params → RO-RO (receive object, return object)
## Data & Classes
- No primitive obsession (use proper types)
- Immutability: readonly, as const
- Composition over inheritance
- Max 200 instructions, max 10 public methods per class
## NestJS
- One module per domain
- DTOs validated with class-validator
- MikroORM: one service per entity
- core/ module: filters, guards, interceptors
- shared/ module: cross-domain logic
## Testing
- Arrange-Act-Assert pattern
- Naming: inputUser, mockRepo, actualResult, expectedOutput
- Acceptance: Given-When-Then
The idea is you paste this whole document into the AI prompt alongside the code you want to refactor. Something like:
Refactor the following NestJS service to comply with
the conventions document below.
CONVENTIONS:
{{ conventions.md }}
REFERENCE FILE (follow this style):
{{ user-profile.service.ts }}
CODE TO REFACTOR:
{{ legacy-service.ts }}
“Am I overthinking this? Do I really need to write all of this down?”
That question I ask myself while writing the document. But honestly after I feed it to the AI together with the code, the difference was pretty clear. Like this kind of thing that keeps happening before:
// Before: AI refactoring without conventions (inconsistent every run)
async function process(id: string, data: any, flag: boolean) {
if (flag) {
const result = await this.repo.findOne({ where: { id } });
if (result) {
result.data = data;
return await this.repo.save(result);
} else {
return null;
}
}
}
After feeding the conventions doc:
// After: AI follows conventions (consistent output)
async function updateEntityById({ id, data }: UpdateEntityParams): Promise<EntityResponse> {
const existingEntity = await this.entityRepository.findOneOrFail(id);
if (!existingEntity) {
return null;
}
existingEntity.assign({ data });
return await this.entityRepository.persistAndFlush(existingEntity);
}
RO-RO pattern, verb-first function name, early return, no deep nesting, proper typing. The output start following the same pattern consistently.
The second lever: reference files
The conventions doc alone was a big improvement, but then I found second trick that makes it even better: also feeding the AI reference files whose style I wanted it to mimic. So instead of just abstract rules, I gave it examples of files that already follow the conventions.
Basically two levers for controlled AI output, (1) an explicit conventions document as source of truth, (2) reference files as style example. The AI could then match the actual coding style, not just reading the rules and interpreting however it want. Together they making the output way more consistent and actually usable for real refactoring work.
I haven’t tinkering around much with different combination of these two at that time, but even the basic setup was already pretty good compared to raw prompting.
Why this matters now
I wrote this conventions doc in January 2025. A few month later, the same idea became mainstream, .cursorrules files for Cursor, CLAUDE.md for Claude Code, project-level instruction files everywhere. The pattern is basically the same: give the AI your rules as a document, not as ad-hoc instructions in every prompt.
It’s cool to see that the industry is moving to the same direction. If you’re doing AI-assisted refactoring and the output keeps being inconsistent, try writing a conventions doc first. For me it was the single most effective thing I did to make AI refactoring actually usable on real codebase.
Sometimes the solution is not about finding better AI tool, it’s about giving the tool better context.