Documentation

Architecture

Co-localisation des composants, groupes de routes, proxy et lib.

Le projet suit une règle stricte : les composants vivent au plus proche de la page qui les utilise. Seul ce qui est réellement partagé entre plusieurs pages remonte dans components/.

Co-localisation

components/ — global uniquement

components/
├── ui/          # Primitives shadcn (button, card, dialog, switch...)
│                # + DataList générique
├── layout/      # Header, Footer, AdminSidebar/Header/Shell, UserSidebar/Header/Shell
├── providers/   # ThemeProvider, NextIntlProvider
├── shared/      # ThemeToggle, LocaleSwitcher, AnnouncementBanner, ImpersonationBanner
└── user/        # UserAvatar, UserMenu, UserDropdown

_components/ — co-localisé

Chaque page (ou groupe de pages) a son _components/ à côté. Le préfixe _ empêche Next.js de créer une route à partir du dossier.

app/[locale]/(public)/_components/
  hero.tsx
  features.tsx
  pricing.tsx
  cta.tsx

Règle décisionnelle

SituationOù ?
1 seule pageapp/.../<page>/_components/
2+ pages distinctescomponents/<thématique>/
Primitive UI réutilisablecomponents/ui/
Layout globalcomponents/layout/

Pas de "components/forms/", pas de "components/admin/" — si un composant n'est utilisé que dans une page, il reste à côté.

Groupes de routes

app/
├── [locale]/
│   ├── (public)/         # Header + Footer publics
│   ├── (auth)/           # Layout centré (login/register/...)
│   ├── (onboarding)/     # Setup post-signup
│   ├── (account)/        # Espace user — UserShell
│   ├── (dashboard)/      # Espace admin — AdminShell
│   ├── (profile)/        # u/[username] — profil public
│   └── docs/             # Fumadocs
├── api/
├── maintenance/          # Hors [locale] — mode maintenance
├── r/[code]/             # Hors [locale] — redirect parrainage
└── layout.tsx            # Root + AnnouncementBanner global

Les pages toujours sous app/[locale]/, sauf : api/, maintenance/, r/, u/, globals.css, not-found.tsx, robots.ts, sitemap.ts.

proxy.ts (ex middleware.ts)

Next.js 16 a renommé middleware.ts en proxy.ts. Combine :

  1. Auth.js (récup session JWT)
  2. next-intl (matching locale, redirect)
  3. Maintenance check (redirect vers /maintenance si non-admin)
  4. 2FA gate (redirect vers /two-factor si pending2fa)
  5. Update UserSession.lastActiveAt

Toujours utiliser getAppUrlFromRequest(req) pour construire des URLs absolues — pas req.url ni req.nextUrl.origin (retournent localhost en LAN).

lib/

lib/
├── auth.ts              # Auth.js config + JWT callbacks + impersonation
├── auth-lockout.ts      # Brute-force protection
├── two-factor.ts        # Code 2FA email
├── prisma.ts            # Singleton Prisma
├── email.ts             # Senders + sendTemplated
├── email-templates.ts   # DEFAULT_TEMPLATES + types
├── plans.ts             # CRUD + cache plans
├── plans-display.ts     # Helpers rendu
├── coupons.ts           # validateCoupon + redeemCoupon
├── site-config.ts       # SiteConfig + pickLocalized
├── audit.ts             # logAudit
├── notifications.ts     # In-app notifications
├── geoip.ts             # IP → country/city
├── sessions.ts          # UserSession helpers
├── referral.ts          # Codes + cookie ref
├── username.ts          # Génération + validation
├── url.ts               # getAppUrl, getAppUrlFromRequest
├── api-key-auth.ts      # Auth via header Bearer
├── dashboard-stats.ts   # Stats live (cache 60s)
├── money.ts             # formatMoney(cents, currency, locale)
├── copy.ts              # Clipboard cross-context (LAN sans HTTPS)
├── onesignal.ts         # Push notifications (optionnel)
└── utils.ts             # cn() (clsx + tailwind-merge)

Providers (root layout)

app/layout.tsx empile :

<NextIntlProvider>
  <ThemeProvider>
    <SessionProvider>
      <AnnouncementBanner />
      <ImpersonationBanner />
      {children}
      <Toaster />
    </SessionProvider>
  </ThemeProvider>
</NextIntlProvider>

Conventions

  • Server Components par défaut, "use client" seulement si nécessaire
  • Zod systématique sur API routes et forms
  • Pas de string en dur — tout passe par useTranslations / getTranslations (voir i18n)
  • Pas de fallbacks défensifs ni de validation pour cas qui ne peuvent pas arriver — le typage Prisma + Zod garantit l'invariant
  • API routes : auth() en première ligne, role check pour admin
  • 100% TypeScript, zéro .js

Allez plus loin

On this page