Writing code that works is one thing. Writing code that your future self (and your teammates) can actually understand six months later is another. Over the years, I've distilled my approach down to six golden rules that I follow on every project.
1. SOC — Separation of Concerns
Each module, component, or function should have one clear responsibility. When your UI component is also fetching data, transforming it, and handling business logic, you've got a problem.
In practice, this means splitting your React components from your data-fetching layer, keeping your API routes thin, and letting each piece of your architecture do one thing well. A well-separated codebase is easier to test, easier to refactor, and easier to reason about.
2. DYC — Document Your Code
Code without context is a puzzle nobody asked to solve. You don't need to comment every line, but you do need to explain the why behind non-obvious decisions.
Good documentation includes:
- Inline comments for complex business logic or workarounds
- README files that explain how to set up and run the project
- Type annotations that serve as living documentation (TypeScript is your friend here)
The best code is self-documenting through clear naming, but when intent isn't obvious, write it down.
3. DRY — Don't Repeat Yourself
If you find yourself copying and pasting the same block of code, stop. Extract it into a shared function, a custom hook, or a utility module.
That said, don't take DRY to the extreme. Premature abstraction is worse than mild duplication. If two pieces of code look similar but serve different purposes and are likely to diverge, it's fine to keep them separate. The rule of three is a good heuristic: abstract on the third occurrence, not the second.
4. KISS — Keep It Simple, Stupid
The simplest solution that solves the problem is almost always the right one. Over-engineering is the silent killer of developer productivity.
Before reaching for a complex state management library, ask: would useState and context work here? Before writing a custom caching layer, ask: does my framework already handle this? Complexity should be earned, not assumed.
5. TDD — Test-Driven Development
Write the test first, then write the code to make it pass. This flips the typical workflow and forces you to think about the expected behavior before the implementation.
Even if you don't follow strict TDD on every feature, the mindset is invaluable:
- Tests become your specification
- Regressions get caught early
- Refactoring becomes safe because you have a safety net
At minimum, cover your critical paths with unit tests and your key user flows with integration tests.
6. YAGNI — You Ain't Gonna Need It
Don't build features or abstractions for hypothetical future requirements. Build what you need right now, ship it, and iterate.
I've seen entire architectures crumble under the weight of "what if we need this later" thinking. Every unnecessary abstraction adds cognitive overhead, increases the surface area for bugs, and slows down the team. If you need it later, build it later — you'll have better context then anyway.
Final Thoughts
These six principles aren't just theoretical — they're the foundation of how I approach every codebase I touch. Whether I'm building a greenfield Next.js app or migrating a legacy PHP monolith, these rules keep the code clean, the team productive, and the product stable.
Clean code isn't about perfection. It's about respect — for the codebase, for your teammates, and for your future self.