# 17. Coding Standards Cette section définit les standards de code minimaux mais critiques pour le développement. Ces règles sont conçues pour les agents IA et les développeurs. ## Critical Fullstack Rules | Rule | Description | |------|-------------| | **Type Sharing** | Toujours définir les types partagés dans `src/types/` et les importer dans les composants et server functions. Ne jamais dupliquer les interfaces. | | **Server Functions Only** | Ne jamais faire d'appels HTTP directs (fetch, axios). Toujours utiliser les server functions importées depuis `@/server/*`. | | **Zod Validation** | Toute entrée utilisateur doit être validée avec un schéma Zod. Le schéma doit être colocalisé avec la server function. | | **Prisma Transactions** | Les opérations multi-tables doivent utiliser `prisma.$transaction()` pour garantir l'atomicité. | | **Cache Invalidation** | Après chaque mutation, invalider les caches affectés via `cacheService.invalidate()` et `queryClient.invalidateQueries()`. | | **Error Boundaries** | Chaque page doit être wrappée dans un ErrorBoundary. Les erreurs serveur doivent utiliser les classes de `@/lib/errors.ts`. | | **No Direct State Mutation** | Ne jamais muter l'état directement. Utiliser les setters de useState ou les mutations TanStack Query. | | **URL State for Filters** | Les filtres et la pagination doivent être stockés dans l'URL via TanStack Router, pas dans le state local. | | **Optimistic Updates Pattern** | Pour les actions fréquentes (toggle, update), utiliser les optimistic updates de TanStack Query. | | **Component Colocation** | Les hooks, types et helpers spécifiques à un composant doivent être dans le même dossier. | ## Naming Conventions | Element | Frontend | Backend | Example | |---------|----------|---------|---------| | Components | PascalCase | — | `CharacterCard.tsx` | | Component files | kebab-case ou PascalCase | — | `character-card.tsx` | | Hooks | camelCase + use prefix | — | `useCharacters.ts` | | Server Functions | camelCase | camelCase | `getCharacters`, `createTeam` | | Server Function files | kebab-case | kebab-case | `characters.ts` | | Types/Interfaces | PascalCase | PascalCase | `Character`, `CreateCharacterInput` | | Enums | PascalCase | PascalCase | `CharacterClass`, `TeamType` | | Constants | SCREAMING_SNAKE | SCREAMING_SNAKE | `MAX_TEAM_SIZE` | | Database tables | — | snake_case | `character_progressions` | | Database columns | — | snake_case | `created_at`, `account_id` | | URL routes | kebab-case | kebab-case | `/characters`, `/bulk-progressions` | | Query keys | camelCase arrays | — | `['characters', 'list', filters]` | | CSS classes | Tailwind utilities | — | `bg-card text-foreground` | ## File Structure Conventions ### Component File Structure ```typescript // 1. Imports (grouped) import { useState } from 'react'; // React import { useQuery } from '@tanstack/react-query'; // External libs import { Button } from '@/components/ui/button'; // Internal UI import { getCharacters } from '@/server/characters'; // Server functions import type { Character } from '@/types'; // Types (last) // 2. Types (if not in separate file) interface CharacterListProps { serverId?: string; } // 3. Constants (if any) const PAGE_SIZE = 20; // 4. Component export function CharacterList({ serverId }: CharacterListProps) { // 4a. Hooks (in order: router, query, state, effects, callbacks) const navigate = useNavigate(); const { data, isLoading } = useQuery({ ... }); const [selected, setSelected] = useState([]); // 4b. Early returns if (isLoading) return ; if (!data) return ; // 4c. Render return (
{/* JSX */}
); } // 5. Sub-components (if small and only used here) function CharacterRow({ character }: { character: Character }) { return ...; } ``` ### Server Function File Structure ```typescript // 1. Imports import { createServerFn } from '@tanstack/start'; import { z } from 'zod'; import { prisma } from '@/lib/prisma'; import { cacheService } from '@/lib/cache'; import { NotFoundError, ValidationError } from '@/lib/errors'; // 2. Schemas (colocated with functions) const createCharacterSchema = z.object({ name: z.string().min(1).max(50), class: z.nativeEnum(CharacterClass), level: z.number().int().min(1).max(200), serverId: z.string().uuid(), accountId: z.string().uuid(), }); // 3. Server Functions (exported) export const getCharacters = createServerFn('GET', async (filters) => { // Implementation }); export const createCharacter = createServerFn('POST', async (input) => { const data = createCharacterSchema.parse(input); // Implementation }); // 4. Helper functions (private, at bottom) function buildWhereClause(filters: Filters): Prisma.CharacterWhereInput { // Implementation } ``` ## Code Quality Rules ### TypeScript ```typescript // ✅ DO: Explicit return types for exported functions export function calculateTotal(items: Item[]): number { return items.reduce((sum, item) => sum + item.amount, 0); } // ❌ DON'T: Implicit any function processData(data) { ... } // ✅ DO: Use type imports import type { Character } from '@/types'; // ❌ DON'T: Import types as values import { Character } from '@/types'; // ✅ DO: Discriminated unions for state type State = | { status: 'loading' } | { status: 'error'; error: Error } | { status: 'success'; data: Character[] }; // ❌ DON'T: Optional properties for state type State = { loading?: boolean; error?: Error; data?: Character[]; }; ``` ### React ```typescript // ✅ DO: Named exports for components export function CharacterCard({ character }: Props) { ... } // ❌ DON'T: Default exports (except for routes) export default function CharacterCard() { ... } // ✅ DO: Destructure props function CharacterCard({ character, onSelect }: Props) { ... } // ❌ DON'T: Props object function CharacterCard(props: Props) { const character = props.character; } // ✅ DO: useCallback for handlers passed to children const handleSelect = useCallback((id: string) => { setSelected(prev => [...prev, id]); }, []); // ✅ DO: useMemo for expensive computations const filteredCharacters = useMemo( () => characters.filter(c => c.level >= 180), [characters] ); // ❌ DON'T: Inline objects/arrays in props (causes re-renders) // Creates new array each render ``` ### Prisma ```typescript // ✅ DO: Select only needed fields const characters = await prisma.character.findMany({ select: { id: true, name: true, level: true, server: { select: { name: true } }, }, }); // ❌ DON'T: Fetch everything const characters = await prisma.character.findMany({ include: { server: true, account: true, progressions: true, currencies: true, teamMemberships: true, }, }); // ✅ DO: Use transactions for related operations await prisma.$transaction([ prisma.teamMember.deleteMany({ where: { teamId } }), prisma.team.delete({ where: { id: teamId } }), ]); // ✅ DO: Count separately from data const [data, total] = await Promise.all([ prisma.character.findMany({ take: 20 }), prisma.character.count(), ]); ``` ## Import Order ESLint avec `eslint-plugin-import` applique cet ordre : ```typescript // 1. React import { useState, useCallback } from 'react'; // 2. External packages (alphabetical) import { useQuery, useMutation } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { z } from 'zod'; // 3. Internal absolute imports (alphabetical by path) import { Button } from '@/components/ui/button'; import { DataTable } from '@/components/data-table/data-table'; import { useCharacters } from '@/hooks/use-characters'; import { prisma } from '@/lib/prisma'; import { getCharacters } from '@/server/characters'; // 4. Relative imports import { CharacterRow } from './character-row'; // 5. Type imports (at the end) import type { Character, CharacterFilters } from '@/types'; ``` ## ESLint & Prettier Config ### .eslintrc.cjs ```javascript module.exports = { root: true, env: { browser: true, es2022: true, node: true }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'prettier', ], parser: '@typescript-eslint/parser', parserOptions: { ecmaVersion: 'latest', sourceType: 'module', project: './tsconfig.json', }, plugins: ['@typescript-eslint', 'import'], rules: { // TypeScript '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], '@typescript-eslint/consistent-type-imports': 'error', '@typescript-eslint/no-explicit-any': 'warn', // Import order 'import/order': [ 'error', { groups: [ 'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'type', ], 'newlines-between': 'always', alphabetize: { order: 'asc' }, }, ], // React 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', // General 'no-console': ['warn', { allow: ['warn', 'error'] }], }, ignorePatterns: ['node_modules', '.output', 'dist'], }; ``` ### .prettierrc ```json { "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "es5", "printWidth": 100, "plugins": ["prettier-plugin-tailwindcss"] } ``` ## Git Commit Standards ```bash # Format (): # Types feat # New feature fix # Bug fix docs # Documentation style # Formatting (no code change) refactor # Refactoring test # Adding tests chore # Build, deps, tooling # Scopes (optional) characters, teams, accounts, progressions, currencies ui, api, db, auth, dofusdb # Examples feat(characters): add bulk progression update fix(teams): validate account constraint on member add refactor(api): extract prisma helpers test(teams): add integration tests for constraints chore(deps): upgrade TanStack Query to v5.17 ``` ---