Aller au contenu
Lantorian

Next.js9 min de lecture

Structure d'un projet Next.js

Le même squelette sur tous nos fronts : App Router, Server Components par défaut, code rangé par fonctionnalité. Tu sais où chercher avant même d'ouvrir le projet.

Notre stack front

BesoinChoix standard
FrameworkNext.js 16 (App Router, Turbopack), React 19.2, TypeScript strict
InterfaceTailwind CSS 4 et composants shadcn/ui copiés dans components/ui
Formulaires et validationZod 4 + React Hook Form, Server Actions
Traductionsnext-intl 4, fichiers JSON par langue et par écran
Donnéesfetch côté serveur vers l'API Laravel, TanStack Query seulement pour le temps réel côté client
TestsVitest + Testing Library, Playwright pour les parcours
QualitéESLint (flat config), Prettier, Husky + lint-staged

L'arborescence

Le dossier app ne contient que du routage. Le code métier vit dans features, un dossier par fonctionnalité, pour que tout ce qui concerne les factures soit au même endroit.

Server ou Client Component ?

Tout composant est un Server Component par défaut : il s'exécute sur le serveur et n'envoie pas de JavaScript au navigateur. On n'ajoute "use client" que lorsque c'est indispensable, sur la plus petite feuille possible.

Exemples :
  1. Utilise-t-il useState, useEffect ou un autre hook d'état ?
Réponds aux questions ou choisis un exemple. La première réponse « oui » suffit à décider.

À éviter

app/[locale]/(app)/invoices/page.tsx
'use client'; // the whole page ships to the browser

export default function InvoicesPage() {
  const [invoices, setInvoices] = useState([]);

  useEffect(() => {
    fetch('/api/invoices')          // waterfall after hydration
      .then((r) => r.json())
      .then(setInvoices);
  }, []);

  return <InvoiceTable invoices={invoices} />;
}

À faire

app/[locale]/(app)/invoices/page.tsx
// Server Component by default: no 'use client'
export default async function InvoicesPage() {
  const invoices = await getInvoices(); // runs on the server

  return (
    <>
      <InvoiceFilters />               {/* small client leaf */}
      <InvoiceTable invoices={invoices} />
    </>
  );
}

Le modèle à reproduire

La page serveur lit les searchParams, délègue le chargement à un composant enveloppé dans Suspense et n'hydrate que le filtre interactif.

src/app/[locale]/(app)/invoices/page.tsx
import { getTranslations } from 'next-intl/server';
import { Suspense } from 'react';
import { InvoiceFilters } from '@/features/invoices/components/invoice-filters';
import { InvoiceTable } from '@/features/invoices/components/invoice-table';
import { InvoiceTableSkeleton } from '@/features/invoices/components/invoice-table.skeleton';

type Props = {
  params: Promise<{ locale: string }>;
  searchParams: Promise<{ status?: string; page?: string }>;
};

export default async function InvoicesPage({ searchParams }: Props) {
  const { status, page } = await searchParams; 
  const t = await getTranslations('invoices');

  return (
    <>
      <h1 className="text-2xl font-semibold">{t('title')}</h1>
      <InvoiceFilters />  {/* client leaf: interactive */}
      <Suspense key={`${status}-${page}`} fallback={<InvoiceTableSkeleton />}>
        <InvoiceTable status={status} page={Number(page ?? 1)} />  {/* server: fetches */}
      </Suspense>
    </>
  );
}

TypeScript et conventions

Le mode strict attrape les erreurs avant la revue. noUncheckedIndexedAccess oblige à gérer le cas où un élément de tableau n'existe pas.

tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "exactOptionalPropertyTypes": false,
    "paths": { "@/*": ["./src/*"] }
  }
}
  • Exports nommés partout, sauf page.tsx, layout.tsx et les fichiers spéciaux de Next.js.
  • Props typées avec un type Props au-dessus du composant, pas d'interface vide.
  • Imports absolus avec @/, jamais de ../../../.
  • Pas de couleur en dur : uniquement les tokens Tailwind du thème (bg-primary, text-muted-foreground).
  • Les secrets ne sont lus que dans des fichiers serveur. Seules les variables NEXT_PUBLIC_ arrivent au navigateur, et elles sont publiques.