name: oliwer-code-style description: > Applique le style de code personnel d'Oliwer pour tout projet web TypeScript/JavaScript (React, Next.js, Vite, Hono, Prisma).
TOUJOURS utiliser ce skill quand :
- Tu generes du code TS/JS/TSX/JSX (composants React, hooks, services, controleurs, routes, etc.)
- Tu reviewes ou corriges du code existant TypeScript/JavaScript
- Tu proposes une architecture de projet, une structure de dossiers, ou un decoupage de modules
- Tu travailles sur un projet avec Hono, Prisma, Next.js, Vite, ou React
- L'utilisateur demande comment structurer une feature, un module, un service, ou un composant
Style de Code Personnel JS/TS
Dev expert. Respecte conventions strictes pour tout projet web (React, NextJS, Vite, Hono).
0. Regles de Style du Texte Genere
Ces regles s'appliquent a tout texte produit par Claude : commentaires de code, messages d'erreur, JSDoc, logs, prose.
- Apostrophes : Ne jamais ecrire
'(apostrophe typographique ou droite) dans du texte genere. Utiliser'a la place. Exemples :J'ai,l'utilisateur,n'existe pas,c'est. - Tiret cadratin : Ne jamais utiliser
--ou---comme separateur dans du texte. Utiliser:ou reformuler la phrase. - Ces regles ne s'appliquent pas au contenu des blocs de code (les strings JS/TS, les valeurs, les templates litteraux gardent leur syntaxe normale).
// ❌ A eviter dans commentaires et JSDoc
// Valide l'email — retourne null si invalide
// J'ai choisi cette approche car...
// ✅ Correct
// Valide l'email. Retourne null si invalide.
// J'ai choisi cette approche car...
// Les strings dans le code restent normales
const msg = "L'email est invalide" // OK, c'est du code
1. Architecture et Paradigme
- General : Fonctions flechees (
const myFunc = () => {}) pour composants React, utilitaires, logique fonctionnelle. - Frontend (Hors NextJS) : Architecture "Features" : code groupe par fonctionnalite, pas par type de fichier.
- API/Backend (Hono/Prisma) : OO (Classes) pour controleurs/services. Structure modulaire par ressource.
- Server/Client (Next.js) : Tout composant = Server Component par defaut.
'use client'uniquement si hooks, evenements DOM ou state local. Jamais sur layout sauf necessite absolue.
// ✅ Structure backend modulaire (Hono)
src/
├── modules/
│ └── user/
│ ├── user.controller.ts # Classe : orchestration requete/reponse
│ ├── user.service.ts # Classe : logique metier pure
│ ├── user.repository.ts # Classe : acces Prisma
│ ├── user.routes.ts # Fonctions flechees : declaration routes Hono
│ └── index.ts # Barrel export
├── middlewares/
└── index.ts
// ✅ Structure feature-based (React/Vite)
src/
├── features/
│ └── auth/
│ ├── components/
│ ├── hooks/
│ ├── utils/
│ └── index.ts
├── components/ui/
└── lib/
2. Imports et Exports
- Barrel files (
index.ts) toujours. Importer depuis barrel, jamais depuis fichier direct.
// ✅ Bien
import { UserService, AuthService } from '@/modules/user'
import { Button, Input } from '@/components/ui'
// ❌ Eviter
import { UserService } from '@/modules/user/user.service'
import { Button } from '@/components/ui/Button'
3. Typage TypeScript et Validation
anyINTERDIT. Generiques ouunknownsi necessaire.interfacepour objets extensibles.typepour unions/intersections/alias primitifs.enumpour valeurs fixes connues a la compilation.- Zod pour toute validation runtime (formulaires, body, API externes, env vars).
// ✅ interface pour les objets
interface User {
id: string
email: string
role: UserRole
}
// ✅ type pour les unions
type Status = 'active' | 'inactive' | 'pending'
// ✅ enum pour les valeurs fixes
enum UserRole {
Admin = 'ADMIN',
Member = 'MEMBER',
Guest = 'GUEST',
}
// ✅ Zod pour validation + inference de type
const CreateUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
role: z.nativeEnum(UserRole).default(UserRole.Member),
})
type CreateUserDto = z.infer<typeof CreateUserSchema>
// ✅ Variables d'environnement typees et validees
// src/env.ts
const EnvSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
})
export const env = EnvSchema.parse(process.env)
// Usage : import { env } from '@/env'
4. Gestion des Erreurs
- Backend (Hono) : Middleware d'erreur global + classes typees heritant
Error. - Frontend : Retourner objet discrimine depuis Server Actions/fetch. Pas de
throwcote client. - Async/await toujours. Jamais
.then()/.catch().try/catchexplicite.
// ✅ Classes d'erreurs typees (backend)
class AppError extends Error {
constructor(
public readonly message: string,
public readonly statusCode: number,
public readonly code: string,
) {
super(message)
}
}
class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, 404, 'NOT_FOUND')
}
}
// ✅ Resultat discrimine (frontend / Server Actions)
type Result<T> = { success: true; data: T } | { success: false; error: string }
const createUser = async (dto: CreateUserDto): Promise<Result<User>> => {
try {
const user = await userService.create(dto)
return { success: true, data: user }
} catch (error) {
return { success: false, error: "Impossible de creer l'utilisateur." }
}
}
// Cote composant
const result = await createUser(formData)
if (!result.success) {
setError(result.error)
return
}
redirect(`/users/${result.data.id}`)
5. Conventions de Nommage
PascalCase: composants, classes, interfaces, enums + fichiers (UserCard.tsx,UserService.ts).camelCase: hooks, utilitaires, variables, fonctions + fichiers (useAuth.ts,formatDate.ts).kebab-case: fichiers/dossiers routeur Next.js (mon-profil/page.tsx,user-settings/layout.tsx).SCREAMING_SNAKE_CASE: constantes globales (MAX_RETRY_COUNT,API_BASE_URL).
// ✅ Nommage correct
src/
├── app/ # Next.js App Router
│ └── user-settings/
│ └── page.tsx # kebab-case
├── features/auth/
│ ├── components/
│ │ └── LoginForm.tsx # PascalCase
│ ├── hooks/
│ │ └── useSession.ts # camelCase
│ └── utils/
│ └── formatAuthError.ts # camelCase
└── lib/
└── constants.ts # MAX_UPLOAD_SIZE = 10_000_000
6. Performance React
- React Compiler (
experimental.reactCompiler: true) : Pas deuseMemo/useCallback/React.memomanuel : le compiler insere automatiquement. - Sans compiler (React <=18) :
useMemo/useCallbackuniquement si calcul couteux (profile) ou lib tierce compare par reference (DnD, chart, map). - Preferer composition a memorisation manuelle.
// ✅ Avec React Compiler : ecriture naturelle, le compiler optimise
const ProductList = ({ products, onSelect }: ProductListProps) => {
const sorted = products.slice().sort((a, b) => a.name.localeCompare(b.name))
const handleSelect = (id: string) => onSelect(id)
return <ul>{sorted.map(p => <ProductItem key={p.id} product={p} onSelect={handleSelect} />)}</ul>
}
// ✅ Sans compiler : memorisation manuelle ciblee apres profiling
const ProductList = ({ products, onSelect }: ProductListProps) => {
const sorted = useMemo(
() => products.slice().sort((a, b) => a.name.localeCompare(b.name)),
[products],
)
const handleSelect = useCallback((id: string) => onSelect(id), [onSelect])
return <ul>{sorted.map(p => <ProductItem key={p.id} product={p} onSelect={handleSelect} />)}</ul>
}
7. Hooks Personnalises
- Prefixe
use+PascalCase:useUserSession,useCartItems. - 1 hook = 1 responsabilite. Extraire des que ~3 hooks natifs lies au meme domaine.
- Dans
src/hooks/(global) ousrc/features/<name>/hooks/(local) + barrel export.
// ✅ useCartItems.ts
const useCartItems = () => {
const [items, setItems] = useState<CartItem[]>([])
const [isLoading, setIsLoading] = useState(false)
const addItem = async (productId: string) => {
setIsLoading(true)
const result = await cartService.add(productId)
if (result.success) setItems(prev => [...prev, result.data])
setIsLoading(false)
}
const removeItem = (id: string) => {
setItems(prev => prev.filter(item => item.id !== id))
}
return { items, isLoading, addItem, removeItem }
}
8. UI et Composants (React / NextJS)
- Tailwind CSS exclusivement. Pas de styles inline sauf valeurs dynamiques impossibles en classes.
- Variantes conditionnelles complexes :
clsxoucva. - Verifier si composant existe avant d'en creer un. Tout reutilisable dans
components/ui/.
// ✅ Variantes avec cva
import { cva, type VariantProps } from 'class-variance-authority'
import { clsx } from 'clsx'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors',
{
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
ghost: 'bg-transparent hover:bg-neutral-100 text-neutral-700',
destructive: 'bg-red-600 text-white hover:bg-red-700',
},
size: {
sm: 'h-8 px-3',
md: 'h-10 px-4',
lg: 'h-12 px-6',
},
},
defaultVariants: { variant: 'primary', size: 'md' },
},
)
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = ({ variant, size, className, ...props }: ButtonProps) => (
<button className={clsx(buttonVariants({ variant, size }), className)} {...props} />
)
9. Qualite de Code (Clean Code)
- SRP : 1 fonction/composant = 1 tache.
- DRY : Extraire logique recurrente en helpers, services ou hooks.
- Early Returns : Cas d'erreur en premier, reduit l'indentation.
- Nommage descriptif : Noms longs/clairs, pas d'abreviations.
// ❌ Mauvais : pas d'early return, nommage opaque, logique melangee
const proc = async (d: unknown) => {
if (d) {
const parsed = schema.safeParse(d)
if (parsed.success) {
const u = await db.user.findUnique({ where: { email: parsed.data.email } })
if (u) {
return u
}
}
}
return null
}
// ✅ Bien : early returns, nommage clair, responsabilite unique
const findUserByEmail = async (rawInput: unknown): Promise<User | null> => {
if (!rawInput) return null
const parseResult = FindUserSchema.safeParse(rawInput)
if (!parseResult.success) return null
const user = await db.user.findUnique({ where: { email: parseResult.data.email } })
return user ?? null
}
10. Commentaires et Documentation
- Zero fichier de doc separe (
README.md,docs/) sauf demande explicite. - JSDoc sur classes, interfaces complexes, fonctions critiques : explique le "pourquoi", pas le "quoi".
- Pas de commentaires qui paraphrasent le code.
// ❌ Commentaire inutile : paraphrase le code
// Incremente le compteur de 1
counter++
// ✅ JSDoc utile : explique le "pourquoi" et le contrat
/**
* Valide et normalise un numero SIRET francais.
* Utilise l'algorithme de Luhn modifie requis par l'INSEE.
*
* @param siret - Le numero SIRET brut (espaces toleres)
* @returns Le SIRET normalise sans espaces, ou null si invalide
*/
const validateSiret = (siret: string): string | null => { ... }
11. Ecosysteme et Outillage
bunen priorite. Replipnpmsi necessaire. Prettier + ESLint gerent le formatage.- Recherche dans le code : Utiliser
rg(ripgrep) plutot quegrep. Plus rapide, respecte.gitignorepar defaut, supporte les types de fichiers.
# ✅ Commandes standard
bun install
bun dev
bun run build
bun test
# Repli pnpm si necessaire
pnpm install
pnpm dev
# ✅ Recherche avec ripgrep
rg "useCartItems" src/
rg -t ts "interface User" src/
rg --no-heading -n "TODO" src/
# ❌ Eviter grep pour chercher dans le code source
grep -r "useCartItems" src/
12. Infrastructure et Deploiement (Docker)
- Jamais
rootdans conteneur. Utilisateur dedie non-root, permissions minimales. - Multi-stage build pour minimiser taille image finale.
# ✅ Dockerfile securise avec multi-stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN npm install -g bun && bun install --frozen-lockfile
COPY . .
RUN bun run build
FROM node:20-alpine AS runner
WORKDIR /app
# Creation d'un utilisateur non-root dedie
RUN addgroup --system --gid 1001 appgroup \
&& adduser --system --uid 1001 --ingroup appgroup appuser
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]