MVC
Model · View · Controller
Le pattern architectural le plus répandu du web. Séparer les données, l'affichage et la logique — pour mieux organiser, tester et faire évoluer une application.
C'est quoi MVC ?
MVC est un pattern architectural — une façon d'organiser le code d'une application en trois couches séparées qui communiquent entre elles de manière définie.
Le Model = la cuisine (les données, les recettes, le stockage).
La View = la salle (ce que le client voit : assiette, présentation).
Le Controller = le serveur (reçoit la commande, parle à la cuisine, apporte le plat).
Sans MVC, tout est mélangé dans un seul fichier : requête SQL, calcul métier, HTML. Modifier l'affichage risque de casser la logique. Avec MVC, chaque partie peut évoluer indépendamment.
MVC a été inventé en 1979 par Trygve Reenskaug pour Smalltalk. Il est aujourd'hui utilisé dans Rails, Django, Laravel, Spring, Express, Angular… c'est le pattern de référence du web.
| Couche | Responsabilité | Ne doit PAS |
|---|---|---|
| Model | Données, BDD, règles métier | Générer du HTML |
| Controller | Recevoir, décider, coordonner | Contenir du SQL |
| View | Afficher, formater | Contenir de la logique |
Rôles détaillés — M, V, C
Le modèle représente les données et la logique métier de l'application.
Il contient :
- Les requêtes vers la base de données
- Les règles de validation (email valide, âge > 0…)
- Les calculs et traitements sur les données
- Les relations entre entités (User a des Articles)
Il ne sait pas comment les données seront affichées.
La vue est responsable de la présentation — uniquement ce que l'utilisateur voit.
Elle contient :
- Les templates HTML (Jinja2, EJS, Blade…)
- La mise en forme des données reçues
- Les boucles d'affichage (
for article in articles) - Peu ou pas de logique
Elle ne fait pas de requêtes BDD directement.
Le contrôleur est le chef d'orchestre — il reçoit les requêtes et coordonne.
Il :
- Reçoit la requête HTTP (GET, POST…)
- Valide les données entrantes
- Appelle le bon Model
- Choisit la View et lui passe les données
Il doit rester mince — pas de logique métier complexe.
"Fat Model, Skinny Controller" — principe clé en MVC : toute la logique métier va dans le Model. Le Controller ne fait que router et déléguer. Un Controller de plus de 20 lignes est souvent un signe que de la logique a mal été placée.
Flux d'une requête — pas à pas
GET /articles.ArticleController.index().Article.findAll() au modèle.SELECT * FROM articles et retourne la liste.articles/liste.html.// 1. Routeur — routes/articles.js
router.get('/', ArticleController.index);
// ↑ le routeur délègue au Controller
// 2. Controller — controllers/ArticleController.js
exports.index = async (req, res) => {
const articles = await Article.findAll(); // → Model
res.render('articles/liste', { articles }); // → View
};
// 3. Model — models/Article.js
Article.findAll = async () => {
const [rows] = await db.query('SELECT * FROM articles');
return rows; // données brutes → Controller
};
// 4. View — views/articles/liste.ejs
// <ul>
// <% articles.forEach(a => { %>
// <li><%= a.titre %></li>
// <% }) %>
// </ul>
Erreur classique débutant : mettre du SQL directement dans le Controller, ou faire des calculs métier dans la View. La règle : si ça touche aux données → Model. Si ça formate pour l'écran → View. Si ça orchestre → Controller.
MVC en Python — Flask
from database import db
class Article(db.Model):
__tablename__ = 'articles'
id = db.Column(db.Integer, primary_key=True)
titre = db.Column(db.String(200), nullable=False)
contenu = db.Column(db.Text)
publie = db.Column(db.Boolean, default=False)
# Méthode de classe = logique métier dans le Model
@classmethod
def tous_publies(cls):
return cls.query.filter_by(publie=True).all()
@classmethod
def creer(cls, titre, contenu):
if not titre.strip():
raise ValueError("Le titre ne peut pas être vide")
article = cls(titre=titre, contenu=contenu)
db.session.add(article)
db.session.commit()
return article
from flask import Blueprint, render_template, request, redirect, url_for
from models.article import Article
articles_bp = Blueprint('articles', __name__)
# GET /articles
@articles_bp.route('/articles')
def index():
articles = Article.tous_publies() # ← demande au Model
return render_template( # ← choisit la View
'articles/liste.html',
articles=articles
)
# POST /articles/nouveau
@articles_bp.route('/articles/nouveau', methods=['POST'])
def creer():
titre = request.form.get('titre')
contenu = request.form.get('contenu')
try:
Article.creer(titre, contenu) # ← délègue au Model
return redirect(url_for('articles.index'))
except ValueError as e:
return render_template('articles/nouveau.html', erreur=str(e))
{# La View ne fait qu'afficher — pas de SQL ici ! #}
{% extends "base.html" %}
{% block content %}
<h1>Articles publiés</h1>
{% if articles %}
<ul>
{% for article in articles %}
<li>
<a href="/articles/{{ article.id }}">
{{ article.titre }}
</a>
</li>
{% endfor %}
</ul>
{% else %}
<p>Aucun article pour l'instant.</p>
{% endif %}
{% endblock %}
from flask import Flask
from database import db
from controllers.articles_ctrl import articles_bp
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
db.init_app(app)
app.register_blueprint(articles_bp)
if __name__ == '__main__':
app.run(debug=True)
Flask est micro-framework — il ne force pas MVC, mais on choisit de l'organiser ainsi. Django, lui, impose une structure MVC (qu'il appelle MVT — Model, View, Template — même concept).
MVC en Node.js — Express
const db = require('../config/db');
const Article = {
async findAllPublies() {
const [rows] = await db.query(
'SELECT * FROM articles WHERE publie = 1'
);
return rows;
},
async findById(id) {
const [rows] = await db.query(
'SELECT * FROM articles WHERE id = ?', [id]
);
return rows[0] || null;
},
async creer({ titre, contenu }) {
if (!titre?.trim())
throw new Error('Titre requis'); // validation dans le Model
const [r] = await db.query(
'INSERT INTO articles (titre, contenu) VALUES (?, ?)',
[titre, contenu]
);
return { id: r.insertId, titre, contenu };
},
};
module.exports = Article;
const Article = require('../models/Article');
exports.index = async (req, res, next) => {
try {
const articles = await Article.findAllPublies(); // Model
res.render('articles/liste', { articles }); // View
} catch (err) { next(err); }
};
exports.creer = async (req, res, next) => {
try {
const article = await Article.creer(req.body);
res.redirect('/articles');
} catch (err) {
res.render('articles/nouveau', { erreur: err.message });
}
};
<!-- Vue EJS — affichage uniquement -->
<h1>Articles</h1>
<% if (articles.length) { %>
<ul>
<% articles.forEach(a => { %>
<li><a href="/articles/<%= a.id %>"><%= a.titre %></a></li>
<% }) %>
</ul>
<% } else { %>
<p>Aucun article.</p>
<% } %>
// routes/articles.js
const router = require('express').Router();
const ctrl = require('../controllers/articleCtrl');
router.get('/', ctrl.index);
router.post(/nouveau', ctrl.creer);
module.exports = router;
// index.js — brancher le moteur de templates
app.set('view engine', 'ejs');
app.set('views', './views');
app.use('/articles', require('./routes/articles'));
Structure de projet MVC
Python / Flask
Node.js / Express
Avec une API REST (retournant du JSON plutôt que du HTML), la View disparaît — le Controller retourne directement res.json(). C'est ce qu'on appelle parfois MC (Model-Controller) ou simplement une "architecture en couches".
Avantages & limites
Avantages
| Séparation des responsabilités | Modifier le HTML n'affecte pas la BDD |
| Testabilité | On peut tester le Model sans démarrer le serveur |
| Travail en équipe | Back-end (Model) et front-end (View) en parallèle |
| Réutilisabilité | Un même Model peut servir plusieurs Views |
| Maintenabilité | Chaque bug est localisé dans une couche |
Limites
| Verbosité | Beaucoup de fichiers pour une petite fonctionnalité |
| Couplage Controller↔View | Le Controller choisit encore la View — difficile à tester |
| Inadapté aux UI riches | Avec React/Vue, la View a sa propre logique → MVVM plus adapté |
| Scalabilité | Pour de très grandes apps, MVC devient insuffisant → architecture hexagonale |
Variations — MVP, MVVM, MVI…
| Pattern | Différence clé vs MVC | Utilisé dans |
|---|---|---|
| MVC | Controller choisit la View et injecte les données | Rails, Django, Express (templates), Spring MVC |
| MVP | Le Presenter ne connaît pas la View concrète (interface) → plus testable | Android (ancien), WinForms |
| MVVM | ViewModel expose des données observables — la View se bind automatiquement | Vue.js, React, Angular, Kotlin, WPF |
| MVI | Flux unidirectionnel strict — Intents → Model → View | React + Redux, Kotlin Android |
Ces patterns se succèdent chronologiquement et répondent aux mêmes problèmes fondamentaux — séparer les données de l'affichage. MVVM est couvert dans la page dédiée. La page comparative met tout ça en perspective.
Cheat sheet MVC
🗄️ Model — contient
| Requêtes SQL / ORM | ✓ |
| Validation des données | ✓ |
| Règles métier | ✓ |
| Relations entre entités | ✓ |
| Génération de HTML | ✗ |
| Code HTTP / routes | ✗ |
🖥️ View — contient
| HTML / templates | ✓ |
| Boucles d'affichage | ✓ |
| Formatage (dates, €…) | ✓ |
| Requêtes SQL | ✗ |
| Logique métier | ✗ |
| Appels à la BDD | ✗ |
🎮 Controller — contient
| Réception des requêtes HTTP | ✓ |
| Appel du bon Model | ✓ |
| Choix de la View | ✓ |
| Validation légère (champs requis) | ✓ |
| SQL complexe | ✗ |
| Logique métier lourde | ✗ |
🏗️ Frameworks MVC
| Django | Python — MVT (≈ MVC) |
| Flask | Python — micro, MVC manuel |
| Ruby on Rails | Ruby — MVC strict |
| Laravel | PHP — MVC + Eloquent ORM |
| Spring MVC | Java — MVC enterprise |
| Express + EJS | Node.js — MVC manuel |