Code Structure
Establishing clear and consistent code organization brings long-term benefits to any codebase.
Well-structured code is easier to read, understand, and navigate, which accelerates onboarding, simplifies debugging, and improves collaboration across teams. It also helps prevent technical debt, reduces duplication, and makes scaling the project more predictable and maintainable.
Keep files under a reasonable size
Split files when they grow too large (e.g., 200+ lines). Long files make it hard to locate logic and increase cognitive load.
// UserDashboard.tsx (700 lines)
1UserDashboard.tsx
2useUserData.ts
3userCharts.tsx
4userStats.tsx
Group and sort imports consistently
Imports should be grouped by type (e.g., external libraries, internal modules, styles) and sorted alphabetically within each group. This improves clarity, reduces merge conflicts, and makes file scanning faster.
Recommended order:
External libraries (e.g.
react
,next
,axios
)Internal modules (e.g.
components/
,utils/
,hooks/
)Relative imports (
./SomeComponent
)Styles (
.css
,.scss
)Blank line between groups
1import styles from './App.module.css';
2import React from 'react';
3import { useUser } from '../../hooks/useUser';
4import { Button } from '../components/Button';
5import _ from 'lodash';
6import { formatDate } from '../../utils/date';
1// External
2import React from 'react';
3import _ from 'lodash';
4
5// Internal with Module Path Aliases
6import { Button } from '@/components/Button';
7import { useUser } from '@/hooks/useUser';
8import { formatDate } from '@/utils/date';
9
10// Styles
11import styles from './App.module.scss';
Prefer module path aliases over deep relative imports
Use relative paths only when importing files that are in the same folder or a direct child folder. If you need to reach outside (e.g. ../../../../utils/
), switch to module path aliases for cleaner, more readable imports. This makes the file structure more intuitive and easier to refactor, especially in large codebases.
1import { formatDate } from '../../../../utils/date';
2import { Button } from '../../../components/ui/Button';
1import { formatDate } from '@utils/date';
2import { Button } from '@components/ui/Button';
Use consistent code ordering in files
Follow a consistent order for declarations: types, constants, functions, exports. Helps readability and predictability
1function main() {}
2const VERSION = '1.0';
3type Config = { ... };
4export default main;
1type Config = { ... };
2
3const VERSION = '1.0';
4
5function main() { ... }
6
7export default main;
Favor composition over inheritance
Whenever possible, prefer building behavior through composition — combining smaller functions or modules — instead of using class inheritance.
Inheritance tightly couples classes and often leads to fragile hierarchies that are hard to change or extend. Composition provides more flexibility, better separation of concerns, and easier testing.
1class Animal {
2 makeSound() {
3 console.log("Some sound");
4 }
5}
6
7class Dog extends Animal {
8 makeSound() {
9 console.log("Woof!");
10 }
11}
12
13class GuardDog extends Dog {
14 makeSound() {
15 super.makeSound();
16 console.log("...and growls");
17 }
18}
1function makeSound(animal: string) {
2 if (animal === 'dog') console.log('Woof!');
3}
4
5function withGuardBehavior(fn: () => void) {
6 return () => {
7 fn();
8 console.log('...and growls');
9 };
10}
11
12const guardDogSound = withGuardBehavior(() => makeSound('dog'));
13guardDogSound();
Separate concerns clearly
Organize your code so that each module, function, or layer is responsible for a single concern — whether it’s data access, business logic, input validation, or rendering.
Mixing responsibilities leads to tangled code, makes testing harder, and increases maintenance complexity.
1/*
2 All concerns in one place:
3 Input validation, DB access, hashing, and response logic are tightly mixed
4 Difficult to test or reuse separately
5 Harder to refactor or change requirements
6*/
7
8// routes/userRoute.js
9app.post('/register', async (req, res) => {
10 const { email, password } = req.body;
11
12 if (!email || !password) {
13 return res.status(400).json({ error: 'Missing credentials' });
14 }
15
16 const existing = await db.query('SELECT * FROM users WHERE email = $1', [email]);
17 if (existing.rows.length > 0) {
18 return res.status(409).json({ error: 'User already exists' });
19 }
20
21 const hashed = await bcrypt.hash(password, 12);
22 await db.query('INSERT INTO users (email, password) VALUES ($1, $2)', [email, hashed]);
23
24 res.status(201).json({ message: 'User created' });
25});
1// routes/userRoute.js
2app.post('/register', validate(registerSchema), registerUser);
3
4// validators/registerSchema.js
5export const registerSchema = {
6 body: {
7 email: { type: 'string', required: true },
8 password: { type: 'string', minLength: 8, required: true }
9 }
10};
11
12// controllers/registerUser.js
13export async function registerUser(req, res) {
14 const { email, password } = req.body;
15
16 const exists = await userRepo.existsByEmail(email);
17 if (exists) {
18 return res.status(409).json({ error: 'User already exists' });
19 }
20
21 const hashed = await hashPassword(password);
22 await userRepo.create({ email, password: hashed });
23
24 res.status(201).json({ message: 'User created' });
25}
Document intent, not mechanics
Write comments that explain why something is done, not what is done — the code already shows the “what”.
Use comments to clarify purpose, assumptions, edge cases, or non-obvious decisions. Avoid redundant or obvious comments that duplicate the code.
1// increment i by 1
2i++;
3
4// call the API
5fetch('/api/user');
6
7// loop through array
8for (let i = 0; i < items.length; i++) { ... }
1// This API returns stale data — adding a cache-busting query param
2fetch(`/api/user?_=${Date.now()}`);
3
4// Using a manual loop for better performance on large arrays
5for (let i = 0; i < items.length; i++) { ... }
6
7// Avoid using debounce here — user expects instant feedback
8handleInputChange(value);
Avoid commented-out code
Dead code clutters the file and confuses collaborators. Use version control instead of commenting-out unused logic.
One export per file (unless grouping makes sense)
Keep each file focused and predictable.
Exceptions: utility files, constant groups, or small related functions.
1// Single file
2export function createUser() {}
3export function deleteUser() {}
4export function banUser() {}
1// createUser.ts
2export function createUser() {}
3
4// deleteUser.ts
5export function deleteUser() {}
6
7// Or group when appropriate:
8// userActions.ts
9export const userActions = {
10 create: () => {},
11 delete: () => {},
12 ban: () => {}
13};
Minimize side effects in top-level code
Avoid logic that runs immediately when a module is loaded unless necessary (e.g., fetching, mutating global state). Prefer deferring side effects until explicitly invoked.
1// services/logger.ts
2connectToLogServer(); // runs on import
3
4export function log(message: string) {
5 // ...
6}
1export function initLogger() {
2 connectToLogServer();
3}
4
5export function log(message: string) {
6 // ...
7}
Align closing brackets in multiline expressions
When an expression spans multiple lines (function calls, object literals, arrays, etc.), place the closing bracket directly under the opening bracket or aligned with the start of the expression. This improves visual structure and makes deeply nested code easier to parse.
1const importantRule = {
2 ruleId: 'align-closing-brackets',
3 title: 'Align Closing Brackets',
4 content: [
5 { type: ruleType.TEXT,
6 text: 'When an expression ...',
7 },
8 ],
9};
Use trailing commas in multiline structures
When working with multiline arrays, objects, or parameter lists, always add a trailing comma after the last item. This leads to cleaner version control diffs, easier refactoring, and consistent formatting.
Cleaner diffs in version control (only new lines change)
No syntax errors when reordering or commenting
Visually consistent formatting
Supported in all modern JS/TS environments (ES2017+)
Exceptions: Do not use trailing commas in JSON or in single-line structures (unless your formatter does).
1const user = {
2 name: 'Alice',
3 age: 30,
4};
5
6const roles = [
7 'admin',
8 'editor',
9];
10
11function greet(
12 name: string,
13 age: number,
14) {
15 // ...
16}
End files with a single newline
Always leave exactly one newline character (\n
) at the end of each file. This ensures compatibility with POSIX systems, keeps Git diffs clean, and aligns with formatting tools and linters.
It’s a small but meaningful consistency rule in professional codebases.
1export function add(a, b) {
2 return a + b;
3}⛔
1export function add(a, b) {
2 return a + b;
3}
4⏎