Projet : API blog

Assembler toutes les couches en un projet complet

Bootcode IWA-S04 — Semaine 16, Jour 5

Objectifs du projet

1. Structurer le projet

domain/, infra/, api/

2. Repository pattern

Interface + Postgres + InMemory

3. Mapper pattern

toDomain / fromDomain

4. Use cases + tests

Un use case par action, testé avec InMemory

5. Assembler dans main.ts

Le seul fichier qui connaît toutes les couches

Plan du cours

1

Récapitulatif de la semaine

Couches, Repository, Mapper, Use Case — tout assemblé

2

La structure finale

domain/ (models, repositories, usecases), infra/, api/

3

Le flux complet en live

Requête HTTP → controller → use case → repository → BDD → retour

4

Les tests

Un test par use case avec InMemoryRepository

5

Correspondance avec Bootcode & Bravo

packages/iac/core/ et adapters/ — vous êtes prêts

Module 1

Récapitulatif

Les 4 patterns de la semaine, assemblés

Les 4 patterns de la semaine

🏗️

J1 — Architecture en couches

API, Domain, Infrastructure

📦

J2 — Repository

Interface dans domain, implémentation dans infra

🔄

J3 — Mapper

toDomain / fromDomain entre BDD et domaine

J4 — Use Case

Une classe, une action, execute()

🎯 Aujourd'hui : tout assembler dans un projet

Le flux complet d'une requête

1️⃣

Requête HTTP arrive

→ Express route
2️⃣

Controller reçoit la requête

→ appelle useCase.execute()
3️⃣

Use case exécute la logique

→ validation, règles métier
4️⃣

Repository persiste

→ Mapper convertit, TypeORM écrit
5️⃣

Réponse HTTP renvoyée

→ objet métier → JSON

Module 2

La structure finale

domain/ · infra/ · api/

L'arborescence du projet

src/

domain/

models/

Article.ts

repositories/

ArticleRepository.ts (interface)

usecases/

CreateArticle.ts

GetArticle.ts

DeleteArticle.ts

infra/

entities/

ArticleEntity.ts (TypeORM)

mappers/

ArticleMapper.ts

repositories/

PostgresArticleRepository.ts

InMemoryArticleRepository.ts

api/

controllers/

ArticleController.ts

routes.ts

main.ts (assemblage)

La règle de dépendance

Le domaine est INDÉPENDANT

Pas d'import de TypeORM, Express, ou quoi que ce soit de technique.

API

Connaît Domain

+ Express

Domain

Connaît RIEN

Logique pure

Infra

Connaît Domain

+ TypeORM

✅ Domain au centre, Infra et API s'adaptent à lui

C'est le Dependency Inversion — l'infra dépend du domaine, jamais l'inverse.

Module 3

Le code des couches

Domain · Infra · API

Domain — modèle & interface

// domain/models/Article.ts

export interface Article {

id: string;

title: string;

content: string;

authorId: string;

createdAt: Date;

}

// domain/repositories/ArticleRepository.ts

export interface ArticleRepository {

save(article: Article): Promise<void>;

findById(id: string): Promise<Article | null>;

delete(id: string): Promise<void>;

}

💡 Aucun import technique — que de la logique pure

Domain — le use case

// domain/usecases/CreateArticle.ts

import { Article, ArticleRepository } from "../";

export class CreateArticle {

constructor(private repo: ArticleRepository) {}

async execute(cmd: { title: string; content: string; authorId: string }): Promise<Article> {

const article: Article = {

id: crypto.randomUUID(),

title: cmd.title,

content: cmd.content,

authorId: cmd.authorId,

createdAt: new Date(),

};

await this.repo.save(article);

return article;

}

}

💡 Le use case dépend de l'INTERFACE, pas de l'implémentation

Infra — entité TypeORM & Mapper

// infra/entities/ArticleEntity.ts

@Entity({ name: "articles" })

export class ArticleEntity {

@PrimaryColumn() id: string;

@Column() title: string;

@Column() content: string;

@Column({ name: "author_id" }) authorId: string;

@Column({ name: "created_at" }) createdAt: Date;

}

// infra/mappers/ArticleMapper.ts

export class ArticleMapper {

static toDomain(e: ArticleEntity): Article { /* ... */ }

static fromDomain(a: Article): ArticleEntity { /* ... */ }

}

Infra — PostgresArticleRepository

// infra/repositories/PostgresArticleRepository.ts

import { ArticleRepository, Article } from "../../domain";

import { ArticleMapper } from "../mappers/ArticleMapper";

export class PostgresArticleRepository implements ArticleRepository {

constructor(private db: DataSource) {}

async save(article: Article): Promise<void> {

const entity = ArticleMapper.fromDomain(article);

await this.db.getRepository(ArticleEntity).save(entity);

}

async findById(id: string): Promise<Article | null> {

const entity = await this.db.getRepository(ArticleEntity).findOneBy({ id });

return entity ? ArticleMapper.toDomain(entity) : null;

}

}

💡 Implémente l'interface du domaine + utilise le Mapper

API — ArticleController

// api/controllers/ArticleController.ts

import { CreateArticle, GetArticle } from "../../domain/usecases";

export class ArticleController {

constructor(

private createArticle: CreateArticle,

private getArticle: GetArticle,

) {}

async create(req, res) {

const article = await this.createArticle.execute(req.body);

res.status(201).json(article);

}

async get(req, res) {

const article = await this.getArticle.execute(req.params.id);

res.json(article);

}

}

💡 Le controller ne fait que déléguer à execute()

Module 4

L'assemblage & les tests

main.ts connecte tout · InMemory pour les tests

main.ts — le seul fichier qui connaît tout

// main.ts — assemblage des couches

import { DataSource } from "typeorm";

import { PostgresArticleRepository } from "./infra/repositories/PostgresArticleRepository";

import { CreateArticle, GetArticle } from "./domain/usecases";

import { ArticleController } from "./api/controllers/ArticleController";

const db = new DataSource({ /* config */ });

// Infra : instancie le repository concret

const articleRepo = new PostgresArticleRepository(db);

// Domain : injecte le repo dans les use cases

const createArticle = new CreateArticle(articleRepo);

const getArticle = new GetArticle(articleRepo);

// API : injecte les use cases dans le controller

const controller = new ArticleController(createArticle, getArticle);

// Branchement des routes Express

app.post("/articles", (req, res) => controller.create(req, res));

app.get("/articles/:id", (req, res) => controller.get(req, res));

💡 main.ts = le SEUL endroit qui connaît toutes les couches

Le domaine, lui, ne sait même pas que Postgres existe.

InMemoryArticleRepository — pour les tests

// infra/repositories/InMemoryArticleRepository.ts

export class InMemoryArticleRepository implements ArticleRepository {

private articles: Article[] = [];

async save(article: Article): Promise<void> {

this.articles.push(article);

}

async findById(id: string): Promise<Article | null> {

return this.articles.find(a => a.id === id) ?? null;

}

async delete(id: string): Promise<void> {

this.articles = this.articles.filter(a => a.id !== id);

}

}

💡 Même interface que Postgres — zéro BDD nécessaire

Les tests sont rapides, isolés, et prouvent que l'architecture fonctionne.

Tester un use case avec InMemory

// CreateArticle.test.ts

test("crée et persiste un article", async () => {

const repo = new InMemoryArticleRepository();

const useCase = new CreateArticle(repo);

const article = await useCase.execute({

title: "Hello",

content: "Mon contenu",

authorId: "user-1",

});

expect(article.title).toBe("Hello");

expect(repo.articles).toHaveLength(1);

});

💡 Un test par use case — la preuve que l'architecture fonctionne

Module 5

Correspondance & Bravo

Du projet au monorepo Bootcode

Correspondance avec le codebase Bootcode

Notre projet

src/domain/

src/infra/

src/api/

src/main.ts

Monorepo Bootcode

packages/iac/core/

packages/iac/adapters/

packages/iac/api/

packages/iac/main.ts

💡 Même architecture, à plus grande échelle

Vous pouvez maintenant lire le monorepo Bootcode comme un livre ouvert.

Pièges courants

⚠️ C'est cumulatif

Les étudiants qui n'ont pas suivi les semaines précédentes auront du mal. Revoir J1-J4 si besoin.

⚠️ Le projet final peut sembler gros

Décomposer en étapes claires : 1) structure, 2) domain, 3) infra, 4) api, 5) main.ts, 6) tests.

⚠️ Ne pas oublier les tests

C'est la preuve que l'architecture fonctionne. Un test par use case avec InMemoryRepository.

⚠️ Importer TypeORM dans le domaine

Jamais ! Le domaine reste pur. L'infra s'adapte au domaine, pas l'inverse.

Points clés à retenir

📌 Tout assembler — voir les patterns travailler ensemble

📌 Le domaine est INDÉPENDANT

Pas d'import de TypeORM, Express, ou quoi que ce soit

📌 main.ts connecte les couches

Le seul fichier qui connaît tout

📌 Les tests prouvent l'architecture

Un test par use case avec InMemoryRepository

À retenir !

🏗️

3 couches

API · Domain · Infra

🔄

Repository + Mapper

Interface + conversion

Use cases + execute()

Une classe par action

🧪

Tests = preuve

InMemoryRepository pour chaque use case

🎉 Bravo !

Vous êtes prêts à lire et contribuer au code professionnel

Le monorepo Bootcode n'a plus de secrets pour vous.

Prochaine semaine : de nouveaux défis !

Questions ?

Assemblez votre API blog complète

Semaine 16 terminée — architecture en couches maîtrisée