MVC — Model View Controller

When the Django Template Knew Too Much

Early in my career I built a restaurant-facing dashboard in Django. The views were a mix of database queries, business rules, and template rendering. A single view function would fetch orders, calculate summary totals, apply tax rules, format currency, and pass it all to the template. When the tax rules changed, I had to find every view that applied them.

MVC is the answer to this. It forces a hard separation: the Model knows the data and rules, the View knows nothing except how to display, and the Controller sits between them, translating user actions into model operations and deciding which view to render.

Table of Contents


The Three Roles

┌───────────────────────────────────────────────────────┐
│                   User Interaction                    │
└───────────────────────────┬───────────────────────────┘
                            │ input (click, form submit)

            ┌───────────────────────────┐
            │        Controller         │
            │  - Receives user input    │
            │  - Validates input        │
            │  - Invokes model          │
            │  - Selects view to render │
            └──────────┬───────┬────────┘
                       │       │
            updates    │       │  selects
                       ▼       ▼
            ┌──────────┐     ┌──────────┐
            │  Model   │     │   View   │
            │ - Data   │────▶│ - Render │
            │ - Rules  │reads│ - Display│
            │ - State  │     │ - Format │
            └──────────┘     └──────────┘

Model: Encapsulates data, domain rules, and persistence. Has no knowledge of how it will be displayed. Can be a domain object, a service, or a repository collection.

View: Renders model data. Contains no business logic. May format data for display (currency, dates) but does not calculate, validate, or persist.

Controller: Translates HTTP requests (or UI events) into model operations. Picks the view to render. Contains coordination logic but no domain rules.


Request Flow


MVC in a Web Framework Context

Different frameworks apply MVC slightly differently:

Framework
Model
View
Controller

Django

models.py + managers

Templates

views.py functions / class-based views

Rails

Active Record models

.erb templates

Controllers

Laravel

Eloquent models

Blade templates

Controllers

Express

Service or repository classes

Template engine / JSON

Route handlers

FastAPI

Service/repository classes

Pydantic response schemas

Routers

In REST APIs, the "view" is the serialised JSON response. Pydantic response models or serialiser classes play the view role.


Python Example: FastAPI MVC


TypeScript Example: Express MVC


MVC vs MVP vs MVVM

Concern
MVC
MVP
MVVM

Who handles user input?

Controller

Presenter

ViewModel (via bindings)

View knowledge of Model?

Direct (via data)

None — Presenter mediates

None — binds to ViewModel

Testability of "middle" layer

Medium — needs HTTP / context

High — pure class

High — pure class

Primary context

Server-side web, REST APIs

Android, desktop GUIs

React, Vue, WPF


Common Violations

  • Fat controller: The controller contains business rules (tax calculation, inventory check). Move those to the service / model layer.

  • Fat model: The model contains rendering logic (formatting currency, building HTML). Move that to the view / schema.

  • View calls the database: The template executes a query. This breaks the pattern and makes N+1 query bugs invisible.

  • Controller bypasses the service: The controller calls the ORM directly, duplicating query logic.


Lessons Learned

  • MVC is a guideline, not a law. In practice, the boundary between controller and service blurs. Having a named layer is more valuable than a perfect implementation.

  • Django's "views.py" is actually the controller. Knowing this prevents beginners from putting everything in the template (which Django calls the view).

  • Thin controllers, thick services. The more logic lives in the service layer, the more of it is testable without an HTTP request.

Last updated