Next.js n'est pas votre Backend : DDD et App Router sans la douleur
Mis à jour le 10 janvier 2026

Next.js n'est pas votre Backend : DDD et App Router sans la douleur

Vous avez migré sur l'App Router et vos Server Actions ressemblent à un plat de spaghettis ? C'est normal. Next.js vous pousse à l'erreur architecturale par défaut. Voici comment reprendre le contrôle avec une approche DDD pragmatique, testable et découplée, qui survit à la hype.

J'ai vu trop de projets Next.js devenir des "Big Balls of Mud" en moins de six mois. Le coupable ? La facilité.

L'App Router est fantastique pour le rendu, mais c'est un piège mortel pour votre logique métier. Si vous importez prisma directement dans votre page.tsx ou vos Server Actions, vous avez déjà perdu. Vous ne construisez pas une application, vous construisez un script géant.

Voici comment j'architecture mes applications Next.js critiques, avec PostgreSQL, Redis et BeanstalkMQ, sans me noyer dans la théorie académique.

1. L'Inversion de Dépendance : Votre gilet de sauvetage

Le problème fondamental de Next.js, c'est qu'il veut être tout à la fois. Votre mission est de le remettre à sa place : Next.js est un détail d'implémentation. C'est juste un adapter HTTP/UI.

Votre architecture doit crier ce que fait votre application, pas le framework qu'elle utilise.

Le piège des Server Actions : Ne mettez JAMAIS de logique métier dans un fichier `actions.ts`. Ce sont des contrôleurs HTTP déguisés. Ils doivent uniquement valider les entrées, appeler le Domaine, et retourner une réponse UI.

2. La Structure des Dossiers (La vraie)

Oubliez la structure par défaut. Nous allons séparer le Domaine (la vérité), l'Infrastructure (les outils) et l'Application (Next.js).

src/
├── core/                  # Votre Business Logic (Agnostique)
│   ├── domain/            # Entités, Value Objects, Events
│   │   ├── order.ts
│   │   └── order-repository.interface.ts
│   └── use-cases/         # Orchestration (Application Services)
│       └── create-order.use-case.ts
├── infrastructure/        # Implémentations concrètes
│   ├── database/          # Postgres & Drizzle/Prisma
│   ├── queue/             # BeanstalkMQ adapters
│   └── cache/             # Redis adapters
└── app/                   # Next.js (UI Layer)
    ├── (routes)/...
    └── _di/               # Composition Root (Manuel)
Isolation du Domaine dans l'écosystème Next.js
Isolation du Domaine dans l'écosystème Next.js

3. Le Domaine : Pur et Dur

Dans src/core/domain, il n'y a pas de NextRequest, pas de sql, pas de redis. Juste du TypeScript pur.

// src/core/domain/order.ts
export class Order {
  constructor(
    public readonly id: string,
    public status: 'PENDING' | 'PAID',
    private _items: OrderItem[]
  ) {}

  // La logique vit ICI, pas dans le contrôleur
  markAsPaid() {
    if (this._items.length === 0) throw new BusinessError("Empty order");
    this.status = 'PAID';
  }
}

Si vous ne pouvez pas instancier cette classe sans mocker une base de données, vous avez échoué.

4. Infrastructure : Les mains dans le cambouis

C'est ici que nous touchons à PostgreSQL, Redis et BeanstalkMQ. L'astuce est d'implémenter des interfaces définies dans le domaine.

Persistence (Postgres)

On n'expose pas l'ORM. On expose un Repository.

// src/infrastructure/database/postgres-order.repository.ts
import { OrderRepository } from '@/core/domain/order-repository.interface';

export class PostgresOrderRepository implements OrderRepository {
  constructor(private db: DbClient) {}

  async save(order: Order): Promise<void> {
    // Mapping Domaine -> DB (Anti-Corruption Layer)
    await this.db.insert(orders).values({ ... });
  }
}

Messaging (BeanstalkMQ)

Pour les tâches asynchrones (emails, processing lourd), on utilise Beanstalk. Mais le domaine ne connaît pas Beanstalk. Il connaît EventPublisher.

Astuce de vieux routier : Utilisez le pattern "Transactional Outbox" si possible. Sinon, assurez-vous que votre publication d'événement dans Beanstalk se fait APRÈS la transaction DB.

5. L'Injection de Dépendance sans Framework

Pas besoin de InversifyJS ou NestJS ici. L'App Router de Next.js rend l'injection complexe car les composants sont instanciés par le framework. La solution pragmatique ? Un fichier container.ts singleton.

// src/app/_di/container.ts
import { CreateOrderUseCase } from '@/core/use-cases/create-order.use-case';
import { PostgresOrderRepository } from '@/infrastructure/database/postgres-order.repository';
// ... imports

// Singleton "Poor man's DI"
const db = new DbClient();
const orderRepo = new PostgresOrderRepository(db);
const eventBus = new BeanstalkEventBus();

export const createOrderUseCase = new CreateOrderUseCase(orderRepo, eventBus);

Ensuite, dans votre Server Action :

// src/app/actions/checkout.ts
'use server'
import { createOrderUseCase } from '@/app/_di/container';

export async function checkoutAction(formData: FormData) {
  // 1. Parse & Validate (Zod)
  // 2. Invoke Use Case
  await createOrderUseCase.execute({ ... });
  // 3. Redirect / Revalidate
}

6. Redis : Caching Stratégique

N'utilisez pas Redis partout aveuglément. Dans une approche DDD, je l'utilise souvent comme décorateur de Repository pour les lectures fréquentes (Read Model).

Si vous séparez CQRS (Command Query Responsibility Segregation), vos lectures peuvent taper directement dans Redis ou une vue matérialisée Postgres, contournant complètement le modèle de domaine riche qui est lent à hydrater.

Le Bilan

Cette architecture est verbeuse. Oui. Vous allez écrire plus de fichiers.

Mais la prochaine fois que Vercel changera son API de cache, ou que vous voudrez passer de Beanstalk à SQS, ou que vous devrez écrire des tests unitaires pour une règle métier complexe : vous me remercierez.

Vous ne codez pas pour aujourd'hui. Vous codez pour le pauvre développeur qui reprendra votre code dans 2 ans. Ce développeur, c'est peut-être vous.

Un projet en tête ?

Discutons de vos besoins techniques et voyons comment je peux vous aider à concrétiser votre projet.

Me contacter