CS375: Software Engineering - Model-View-Controller (MVC) Architecture
Activity Goals
The goals of this activity are:- To explain the responsibilities of the model, view, and controller layers and why separating them makes software easier to test, maintain, and divide among a team
- To trace a single HTTP request through the route, controller, and model layers of a web application
- To explain how JSON Web Token (JWT) authentication protects routes using middleware
- To place default values (such as creation timestamps) in the model or controller rather than the view
Supplemental Reading
Feel free to visit these resources for supplemental background reading material.The Activity
Directions
Consider the activity models and answer the questions provided. First reflect on these questions on your own briefly, before discussing and comparing your thoughts with your group. Appoint one member of your group to discuss your findings with the class, and the rest of the group should help that member prepare their response. Answer each question individually from the activity, and compare with your group to prepare for our whole-class discussion. After class, think about the questions in the reflective prompt and respond to those individually in your notebook. Report out on areas of disagreement or items for which you and your group identified alternative approaches. Write down and report out questions you encountered along the way for group discussion.Model 1: MVC as a Restaurant: Separation of Concerns
| Layer | In a Restaurant | In a Web App | Knows About | Must Never |
|---|---|---|---|---|
| View | The menu and the plated dish: what the customer sees | HTML templates, a mobile app, or the JSON an API returns | How to display data it is handed | Compute business logic or touch the database |
| Controller | The waiter: takes the order, relays it, brings back the result | Functions that unpack a request, call model functions, and choose a response and status code | HTTP requests/responses and the model's functions | Contain SQL or produce HTML itself |
| Model | The kitchen: actually prepares the food from the pantry | Functions that read and write the data store (SQL, a file, a cloud service) and enforce data rules | The database schema and business rules | Know anything about HTTP or the user interface |
Questions
- The customer never walks into the kitchen, and the chef never talks to the customer. What is the software equivalent of each of these rules, and what could go wrong if they were violated?
- Suppose the restaurant replaces its paper menu with a tablet (a new view). What has to change in the kitchen? Now suppose your web app adds a mobile app alongside its website. What has to change in the model?
- Your team has three developers. How does this separation let all three work on the same feature at the same time without stepping on each other? Which layer would each person own?
- Which layer(s) can be unit tested without starting a web server or a browser? Why does that matter for your test plan?
Model 2: Tracing One Request: POST /notes
| Step | File (in the JWT example below) | What Happens |
|---|---|---|
| 1 | server.js | POST /notes arrives; the /notes prefix is delegated to noteRoutes |
| 2 | routes/noteRoutes.js | The route table matches POST / and runs the middleware chain: requireAuth, then noteController.createNote |
| 3 | middleware/authMiddleware.js | jwt.verify checks the token signature from the Authorization: Bearer header; valid → req.user is set and next() continues; invalid → 401, stop here |
| 4 | controllers/noteController.js | Validates req.body.text and calls noteModel.createNote(req.user.sub, text) |
| 5 | models/noteModel.js | Writes the note to the data store, stamping createdAt at insert time |
| 6 | back up the chain | The controller sends 201 Created with the new note as JSON (the "view" of an API) |
Questions
- At which step is an unauthenticated request rejected? Which layers never even run in that case, and why is that a good security property?
- A JWT has three parts: a header, a payload (for example
{"sub": 1, "username": "alice", "exp": ...}), and a signature computed with a secret only the server knows. The payload is readable by anyone -- so what stops a user from editing it to become someone else? - Why does the controller take the owner of the new note from
req.user(the verified token) rather than from the request body? - Follow the same style of trace for
POST /auth/login: list the files visited in order and what each contributes.
Why MVC?
As soon as a program has a user interface and stored data and rules connecting them, there is a temptation to write it as one big file where the button-click handler runs the SQL and formats the HTML. It works – once. Then the team grows, the designer wants to restyle every page, the customer wants a mobile app, and the tester wants to test the business rules without clicking through the UI, and the big file fights all of them at once.
Model-View-Controller (MVC) is the standard cure: split the application into three layers with strict rules about who may talk to whom (study the restaurant table in the first model above). The payoff is exactly what a software team needs: layers can be developed in parallel (one teammate per layer), tested in isolation (the model needs no browser; the view needs no database), and replaced independently (swap SQLite for MySQL, or add a mobile view, without touching the other layers). Nearly every web framework you will meet professionally – Express, Django, Rails, Spring, ASP.NET – is organized this way.
Example: A Database-Backed MVC Application
The example below is a complete Express application managing students, courses, and enrollments (the same schema from the databases activity), organized into routes/, controllers/, models/, and views/ folders with a SQLite database and JWT authentication. Browse the folders and notice that every file has exactly one kind of job.
Example: A Minimal MVC Service with JWT Authentication
The second example strips the pattern down to its skeleton so you can trace every line: two resources (/auth and /notes), a JSON-file model (so it runs anywhere with just npm install && node server.js), and JWT login protecting the notes routes. The request-trace table in the second model above walks through this exact code – open the files side by side with the table.
How the JWT Handshake Works
- Login once: the client
POSTs a username and password to/auth/login. The controller asks the user model to verify them, and, on success, signs a token:jwt.sign({ sub: user.id }, SECRET, { expiresIn: '1h' }). The token is just a string the client stores. - Present the token on every request: the client sends
Authorization: Bearer <token>on each call to a protected route. No password is transmitted again, and the server keeps no session state – the token is the proof. - Verify at the door: the
requireAuthmiddleware callsjwt.verify(token, SECRET), which recomputes the signature. A forged or expired token throws, and the middleware answers401before any controller runs. A valid token’s payload becomesreq.user, which downstream code may trust.
Run the example locally and follow the curl transcript in its README: register, log in, create a note with the token, and then try again with the token deliberately corrupted.
Design Guideline: Default Values Belong in the Model (or Controller), Never the View
Here is a mistake that appears in almost every first team project. A table needs a createDate column, so someone adds it to the form:
Bad – the view supplies the timestamp:
<!-- add-note.html -->
<input type="hidden" id="createDate">
<script>
document.getElementById("createDate").value = new Date().toISOString();
</script>
Why is this wrong? The timestamp now depends on the client’s clock (wrong time zones, skewed clocks, users who edit the hidden field in dev tools), it must be re-implemented in every view (web form, mobile app, API client, test script), and any view that forgets it inserts a NULL. “When was this row created?” is a fact about the data, so the data layer should own it.
Good – the model supplies it, in the schema itself:
CREATE TABLE notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
createDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- the INSERT simply omits createDate, and the database fills it in
INSERT INTO notes (text) VALUES (?);
Also acceptable – the controller (the intermediary) supplies it, when the value needs application logic the database cannot express:
// noteController.js
const note = noteModel.createNote(req.user.sub, text); // model stamps createdAt
// or, in the controller: noteModel.insert({ text, createdAt: new Date() });
The rule generalizes beyond timestamps: any value the user did not consciously choose – creation dates, record IDs, the owning user, initial status fields – should be filled in by the model (preferably, via the schema) or the controller, never collected from the view. Both example applications on this page follow this rule; find the places where they do. This guideline also appears on the design review checklist of your Objects/API Summary deliverable.
Your Turn
- In the
DatabaseMVCExample, pick one requirement (“a student enrolls in a course”) and write the six-step request trace for it, in the style of the table above. - Add a
DELETE /notes/:idfeature to theNodeJWTMVCExample: which three files do you touch, and what does each change contribute? Ensure a user can delete only their own notes – which layer enforces that, and why? - Audit your own project design: list every field in your schema that the user does not consciously choose, and state (in your Objects/API Summary) which layer supplies each one.