Skip to content

Commit 3374993

Browse files
committed
feat: add comprehensive code style guide documentation
- Introduced guidelines for file structure, naming conventions, and exports. - Included best practices for components, services, schemas, and testing. - Added detailed examples and anti-patterns for common pitfalls.
1 parent 1a30950 commit 3374993

1 file changed

Lines changed: 334 additions & 0 deletions

File tree

docs/code-style-guide.md

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
# Code Style Guide
2+
3+
## Quick Reference
4+
5+
### File Placement
6+
7+
| Code Type | Location |
8+
|-----------|----------|
9+
| React component | `components/pages/`, `components/shared/`, or `components/ui/` |
10+
| API logic | `services/{provider}/{service}/` |
11+
| Reusable function | `utils/{utilName}/` |
12+
| React hook | `hooks/use{HookName}/` |
13+
| Third-party config | `libs/{libraryName}/` |
14+
15+
### File Extensions
16+
17+
| Extension | Purpose |
18+
|-----------|---------|
19+
| `.ts{x}` | Main code (use `.tsx` only when file contains JSX) |
20+
| `.test.ts` | Tests |
21+
| `.types.ts` | TypeScript types |
22+
| `.utils.ts{x}` | Helper functions |
23+
| `.utils.test.ts` | Tests for utility functions |
24+
| `.schemas.ts` | Zod validation schemas |
25+
| `.schemas.test.ts` | Tests for schemas |
26+
| `.constants.ts{x}` | Static objects and constants |
27+
28+
### Naming Conventions
29+
30+
| Context | Convention | Example |
31+
|---------|------------|---------|
32+
| Component folders | camelCase | `userCard/` |
33+
| Component files | PascalCase | `UserCard.tsx` |
34+
| Everything else | camelCase | `httpClient.ts` |
35+
| Variables/params | Descriptive (no single chars) | `event` not `e` |
36+
| Constants | camelCase | `maxRetries` not `MAX_RETRIES` |
37+
38+
### Exports
39+
40+
All files use named exports:
41+
42+
```typescript
43+
export function Component() {}
44+
```
45+
46+
---
47+
48+
## Directory Structure
49+
50+
```text
51+
src/
52+
├── components/
53+
│ ├── pages/ # Page-level components
54+
│ │ └── userProfilePage/
55+
│ │ ├── UserProfilePage.tsx
56+
│ │ └── profileView/
57+
│ │ ├── ProfileView.tsx
58+
│ │ └── avatarSection/
59+
│ │ └── AvatarSection.tsx
60+
│ ├── shared/ # Reusable across pages
61+
│ └── ui/ # Pure presentation components
62+
├── hooks/
63+
│ └── useDebounce/
64+
│ ├── useDebounce.ts
65+
│ └── useDebounce.test.ts
66+
├── libs/ # Third-party wrappers
67+
├── services/
68+
│ └── hyperion/
69+
│ └── users/
70+
│ ├── users.ts
71+
│ ├── users.schemas.ts
72+
│ └── users.constants.ts
73+
├── styles/
74+
└── utils/
75+
└── date/
76+
├── date.ts # No .utils.ts extension in utils/
77+
└── date.test.ts
78+
```
79+
80+
---
81+
82+
## Component Patterns
83+
84+
### File Organization
85+
86+
Components contain only JSX, Props type, and hooks. Extract everything else:
87+
88+
```text
89+
userCard/
90+
├── UserCard.tsx # Component + Props type only
91+
├── UserCard.types.ts # All other types
92+
├── UserCard.constants.ts # All constants
93+
└── UserCard.utils.ts # All helper functions
94+
```
95+
96+
### Component File
97+
98+
```typescript
99+
// UserCard.tsx
100+
import { formatUserName } from './UserCard.utils';
101+
import type { User } from './UserCard.types';
102+
103+
type Props = { // Never export Props
104+
user: User;
105+
onSelect: (id: string) => void;
106+
};
107+
108+
export function UserCard({ user, onSelect }: Props) {
109+
return <div onClick={() => onSelect(user.id)}>{formatUserName(user.name)}</div>;
110+
}
111+
```
112+
113+
### Supporting Files
114+
115+
```typescript
116+
// UserCard.types.ts
117+
export type User = {
118+
id: string;
119+
name: string;
120+
status: 'active' | 'inactive';
121+
};
122+
123+
// UserCard.constants.ts
124+
export const maxNameLength = 30;
125+
126+
// UserCard.utils.ts
127+
import { maxNameLength } from './UserCard.constants';
128+
129+
export function formatUserName(name: string): string {
130+
return name.length > maxNameLength ? `${name.slice(0, maxNameLength)}...` : name;
131+
}
132+
```
133+
134+
---
135+
136+
## Service Patterns
137+
138+
Services have three files: main functions, schemas, and constants.
139+
140+
### Main Service File
141+
142+
```typescript
143+
// services/hyperion/users/users.ts
144+
import { useMutation, useQuery } from '@tanstack/react-query';
145+
import { http } from '@/utils/httpClient/httpClient';
146+
import { api } from '@/utils/httpClient/httpClient.constants';
147+
import { getUsersKey } from './users.constants';
148+
import { GetUsersResponseSchema } from './users.schemas';
149+
150+
/* POST /api/v1/users */
151+
export async function getUsers() {
152+
return http.get<GetUsersResponseSchema>(api.hyperion.users.v1.list, {
153+
responseSchema: GetUsersResponseSchema,
154+
});
155+
}
156+
157+
export function useGetUsers() {
158+
return useQuery({
159+
queryKey: [getUsersKey],
160+
queryFn: getUsers,
161+
});
162+
}
163+
```
164+
165+
### Schema File
166+
167+
```typescript
168+
// services/hyperion/users/users.schemas.ts
169+
import { z } from 'zod';
170+
171+
export const GetUsersResponseSchema = z.object({
172+
users: z.array(
173+
z.object({
174+
id: z.uuid(),
175+
email: z.email(),
176+
name: z.string(),
177+
}),
178+
),
179+
});
180+
export type GetUsersResponseSchema = z.infer<typeof GetUsersResponseSchema>;
181+
```
182+
183+
### Constants File
184+
185+
```typescript
186+
// services/hyperion/users/users.constants.ts
187+
export const getUsersKey = 'getUsers';
188+
```
189+
190+
---
191+
192+
## Testing Patterns
193+
194+
Use `test.each` with array of objects:
195+
196+
```typescript
197+
import { describe, expect, test } from 'bun:test';
198+
import { formatUserName } from './user';
199+
200+
describe('formatUserName', () => {
201+
test.each([
202+
{
203+
description: 'full name with all parts',
204+
input: 'Smith, John David',
205+
expected: { lastName: 'Smith', firstName: 'John', middleName: 'David' },
206+
},
207+
{
208+
description: 'name without middle',
209+
input: 'Smith, John',
210+
expected: { lastName: 'Smith', firstName: 'John', middleName: undefined },
211+
},
212+
])('should handle $description', ({ input, expected }) => {
213+
expect(formatUserName(input)).toEqual(expected);
214+
});
215+
});
216+
```
217+
218+
---
219+
220+
## TypeScript Rules
221+
222+
- Use `type` for all type definitions (object shapes, unions, and advanced type operations)
223+
- Schema types: `z.infer<typeof Schema>`
224+
- **Schema names and type names should be the same**: In TypeScript, you can have a const and a type with the same name because they exist in different namespaces (value namespace vs type namespace). This keeps naming consistent and clear.
225+
226+
### Schema Pattern
227+
228+
```typescript
229+
// Correct: Schema and type share the same name
230+
export const UserInfoSchema = z.object({
231+
name: z.string().default(''),
232+
email: z.string().default(''),
233+
});
234+
export type UserInfoSchema = z.infer<typeof UserInfoSchema>;
235+
236+
// Usage - import the const, TypeScript automatically uses the type when needed:
237+
import {UserInfoSchema} from './schemas';
238+
239+
// As a value (schema):
240+
const result = UserInfoSchema.parse(data);
241+
// As a type:
242+
function getUser(): UserInfoSchema { ... }
243+
```
244+
245+
```typescript
246+
// Wrong: Different names for schema and type
247+
export const UserInfoSchema = z.object({...});
248+
export type UserInfo = z.infer<typeof UserInfoSchema>; // Don't rename
249+
```
250+
251+
```typescript
252+
// Wrong: Using type aliases when importing
253+
import {UserInfoSchema} from './schemas';
254+
import type {UserInfoSchema as UserInfoSchemaType} from './schemas'; // Don't alias
255+
function getUser(): UserInfoSchemaType { ... } // Just use UserInfoSchema
256+
```
257+
258+
---
259+
260+
## Barrel Files
261+
262+
**Do not use barrel files (`index.ts`/`index.js`) in application code.**
263+
264+
Barrel files are files that only re-export from other modules. They cause problems:
265+
266+
- **Circular imports**: Easy to accidentally create import cycles that crash bundlers
267+
- **Slow development**: Loading a barrel loads all modules it exports, even if you only need one
268+
- **Hard to optimize**: Bundlers struggle to tree-shake and optimize barrel imports
269+
270+
### Wrong
271+
272+
```typescript
273+
// components/ui/index.ts
274+
export { Button } from './button/Button';
275+
export { Input } from './input/Input';
276+
export { Card } from './card/Card';
277+
278+
// Usage creates circular import risk
279+
import { Button } from '@/components/ui';
280+
```
281+
282+
### Right
283+
284+
```typescript
285+
// Import directly from the module
286+
import { Button } from '@/components/ui/button/Button';
287+
import { Input } from '@/components/ui/input/Input';
288+
```
289+
290+
### Exception: NPM Libraries
291+
292+
Barrel files are **only** acceptable as the single entry point for npm packages:
293+
294+
```typescript
295+
// packages/my-library/index.ts (package.json "main" field)
296+
export { Button } from './components/Button';
297+
export { useTheme } from './hooks/useTheme';
298+
```
299+
300+
**Reference**: [Please Stop Using Barrel Files](https://tkdodo.eu/blog/please-stop-using-barrel-files#what-barrels-are-good-for)
301+
302+
---
303+
304+
## Component Hierarchy
305+
306+
Subcomponents belong to the component that imports them:
307+
308+
```text
309+
announcementsPage/
310+
├── AnnouncementsPage.tsx # List page
311+
└── announcementPage/ # Detail page (sibling)
312+
├── AnnouncementPage.tsx
313+
└── announcementForm/ # Child of AnnouncementPage
314+
└── AnnouncementForm.tsx
315+
```
316+
317+
---
318+
319+
## Common Mistakes
320+
321+
| Wrong | Right |
322+
|-------|-------|
323+
| `interface Props {...}` | `type Props = {...}` |
324+
| `export type Props` | `type Props` (no export) |
325+
| Types in component file | Move to `.types.ts` (except Props) |
326+
| `utils/dateUtils/dateUtils.ts` | `utils/date/date.ts` |
327+
| `const MAX_RETRIES = 3` | `const maxRetries = 3` |
328+
| `(e) => handleClick(e)` | `(event) => handleClick(event)` |
329+
| Constants in component | Extract to `.constants.ts` |
330+
| Helpers in component | Extract to `.utils.ts` |
331+
| Multiple components per file | One component per file |
332+
| Barrel files (`index.ts`) | Direct imports from modules |
333+
| Default exports | Named exports |
334+
| Re-exporting types/values | Import directly from source file |

0 commit comments

Comments
 (0)