CS375: Software Engineering - Model-View-Controller (MVC) Architecture

Activity Goals

The goals of this activity are:
  1. 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
  2. To trace a single HTTP request through the route, controller, and model layers of a web application
  3. To explain how JSON Web Token (JWT) authentication protects routes using middleware
  4. 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

LayerIn a RestaurantIn a Web AppKnows AboutMust 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

  1. 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?
  2. 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?
  3. 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?
  4. 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

StepFile (in the JWT example below)What Happens
1server.jsPOST /notes arrives; the /notes prefix is delegated to noteRoutes
2routes/noteRoutes.jsThe route table matches POST / and runs the middleware chain: requireAuth, then noteController.createNote
3middleware/authMiddleware.jsjwt.verify checks the token signature from the Authorization: Bearer header; valid → req.user is set and next() continues; invalid → 401, stop here
4controllers/noteController.jsValidates req.body.text and calls noteModel.createNote(req.user.sub, text)
5models/noteModel.jsWrites the note to the data store, stamping createdAt at insert time
6back up the chainThe controller sends 201 Created with the new note as JSON (the "view" of an API)

Questions

  1. At which step is an unauthenticated request rejected? Which layers never even run in that case, and why is that a good security property?
  2. 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?
  3. Why does the controller take the owner of the new note from req.user (the verified token) rather than from the request body?
  4. 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

  1. 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.
  2. 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.
  3. Verify at the door: the requireAuth middleware calls jwt.verify(token, SECRET), which recomputes the signature. A forged or expired token throws, and the middleware answers 401 before any controller runs. A valid token’s payload becomes req.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

  1. 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.
  2. Add a DELETE /notes/:id feature to the NodeJWTMVCExample: 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?
  3. 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.

Submission

I encourage you to submit your answers to the questions (and ask your own questions!) using the Class Activity Questions discussion board. You may also respond to questions or comments made by others, or ask follow-up questions there. Answer any reflective prompt questions in the Reflective Journal section of your OneNote Classroom personal section. You can find the link to the class notebook on the syllabus.