MVC is a design pattern that divides the application into three interconnected components:
- Model: Represents the application's data and the business rules that govern access to and updates of this data. In many cases, the model component is responsible for retrieving data, processing it, and then storing it. It is independent of the user interface (UI).
- View: Represents the UI of the application. It displays the data that the model contains to the user and sends user commands (e.g., button clicks) to the controller. The view is passive, meaning it waits for the model or controller to give it data to display.
- Controller: Acts as an intermediary between the Model and the View. It listens to events triggered by the View and executes the appropriate response, often resulting in a change in the Model's state. Similarly, when the Model changes (e.g., data is updated), the Controller is responsible for refreshing the View.
Benefits of MVC:
- Separation of Concerns: By separating the application into these components, MVC aids in the organization of code, making it more modular and scalable. Each component has a distinct responsibility.
- Maintainability: With clear separations, developers can work on one aspect of an application (like the UI) without having to touch the data logic code. This separation allows teams to work on different parts of an application simultaneously.
- Flexibility: The View and the Model can evolve separately. Multiple Views can be created from one Model, which is especially useful when you have web, mobile, and other UIs for the same data.
- Reusability: Business logic in the Model can often be reused across different parts of an application or even different projects.
Many popular web development frameworks like Django (Python), Ruby on Rails (Ruby), ASP.NET MVC (C#), and Express with Pug or EJS implement the MVC pattern or variations of it. The MVC pattern has been adapted in slightly different ways by various frameworks, but the core principle remains:
- When a user sends a n HTTP request, the request first reaches the Controller.
- The Controller processes the request, interacts with the Model (which might involve querying a database), and then decides which View should be used to display the resulting data.
- The View takes the data, renders it, and sends the resulting webpage back to the user (server-side rendering, SSR).
graph TD
client[Client]
router[Router]
controller[Controller]
model[Model]
view[View]
database[Database]
client -- HTTP request --> router
router -- Route handling --> controller
controller -- Business logic --> model
model -- SQL data access < --> database
model -- Model data --> controller
controller -- Render --> view
view -- HTML response --> client
When adapted to REST API the view is typically represented by the format of the API response (usually JSON), rather than a traditional user interface which is this case rendered on the client-side (CSR).
graph TD
client[Client]
router[Router]
controller[Controller]
model[Model]
api_response[API Response JSON]
database[Database]
client -- HTTP request --> router
router -- Route handling --> controller
controller -- Business logic --> model
model -- SQL data access < --> database
model -- Model data --> controller
controller -- Prepare response --> api_response
api_response -- JSON response --> client
Type-based folder structure (typical for Express applications):
src
├── api/
app.js ├── controllers/
│ ├── auth-conroller.js
│ └── user-controller.js
│ └── cat-controller.js
├── models/
│ ├── user-model.js
│ └── cat-model.js
├── routes/
│ ├── auth-router.js
│ └── user-router.js
│ └── cat-router.js
└── index.js
Feature-based folder structure:
src/
├── car/
│ ├── controller.js
│ ├── model.js
│ └── routes.js
├── user/
│ ├── controller.js
│ ├── model.js
│ └── routes.js
└── index.js
Both of the folder structures has its benefits. The type-based structure is simple and straightforward, making it easy to navigate for small projects. The feature-based structure, on the other hand, scales better for larger applications by grouping all related files by feature, making the codebase more modular and maintainable.
express.Router is a middleware and more advanced routing system that allows you to modularize your routes into separate files.
src/app.js:
...
import api from './api/index.js';
...
app.use(express.json());
app.use(express.urlencoded({extended: true}));
...
app.use('/api/v1', api);
...
src/api/index.js:
import express from 'express';
import catRouter from './routes/cat-router.js';
const router = express.Router();
// bind base url for all cat routes to catRouter
router.use('/cats', catRouter);
export default router;
src/api/routes/cat-router.js:
import express from 'express';
import {
getCat,
getCatById,
postCat,
putCat,
deleteCat,
} from '../controllers/cat-controller.js';
const catRouter = express.Router();
catRouter.route('/').get(getCat).post(postCat);
catRouter.route('/:id').get(getCatById).put(putCat).delete(deleteCat);
export default catRouter;
src/api/controllers/cat-controller.js:
import {addCat, findCatById, listAllCats} from "../models/cat-model.js";
const getCat = (req, res) => {
res.json(listAllCats());
};
const getCatById = (req, res) => {
const cat = findCatById(req.params.id);
if (cat) {
res.json(cat);
} else {
res.sendStatus(404);
}
};
const postCat = (req, res) => {
const result = addCat(req.body);
if (result.cat_id) {
res.status(201);
res.json({message: 'New cat added.', result});
} else {
res.sendStatus(400);
}
};
const putCat = (req, res) => {
// not implemented in this example, this is future homework
res.sendStatus(200);
};
const deleteCat = (req, res) => {
// not implemented in this example, this is future homework
res.sendStatus(200);
};
export {getCat, getCatById, postCat, putCat, deleteCat};
src/api/models/cat-model.js:
// mock data
const catItems = [
{
cat_id: 9592,
cat_name: 'Frank',
weight: 11,
owner: 3609,
filename: 'f3dbafakjsdfhg4',
birthdate: '2021-10-12',
},
{
cat_id: 9590,
cat_name: 'Mittens',
weight: 8,
owner: 3602,
filename: 'f3dasdfkjsdfhgasdf',
birthdate: '2021-10-12',
},
];
const listAllCats = () => {
return catItems;
};
const findCatById = (id) => {
return catItems.find((item) => item.cat_id == id);
};
const addCat = (cat) => {
const {cat_name, weight, owner, filename, birthdate} = cat;
const newId = catItems[0].cat_id + 1;
catItems.unshift({cat_id: newId, cat_name, weight, owner, filename, birthdate});
return {cat_id: newId};
};
export {listAllCats, findCatById, addCat};
- Create a new branch
Assignment2
- Create a new folder
src
in your project folder and move yourapp.js
file there. - To make the express app easier to test, create
src/index.js
file and importapp.js
from thesrc
folder:import app from './app.js'; const hostname = '127.0.0.1'; const port = 3000; app.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
- Remove the above code from the
app.js
file and add the following code to the end of theapp.js
file:export default app;
- Update scripts in
package.json
to pointsrc/index.js
. - Create a new folder
api
in yoursrc
folder - Create a new folder
routes
in yourapi
folder - Create a new folder
controllers
in yourapi
folder - Create a new folder
models
in yourapi
folder - Based on the examples above create an Express project with the following routes:
GET /api/v1/cat
- returns all catsGET /api/v1/cat/:id
- returns one cat by idPOST /api/v1/cat
- adds a new catPUT /api/v1/cat/:id
- return hard coded json response:{message: 'Cat item updated.'}
DELETE /api/v1/cat/:id
- return hard coded json response:{message: 'Cat item deleted.'}
- Test the endpoints in Postman. Get cats, add a new cat, then get cats again to see if the new cat is added.
- Use the above examples to create routes for users. Create similar dummy data:
const userItems = [ { user_id: 3609, name: 'John Doe', username: 'johndoe', email: '[email protected]', role: 'user', password: 'password', }, etc... ];
- Add the following endpoints:
GET /api/v1/user
- returns all usersGET /api/v1/user/:id
- returns one user by idPOST /api/v1/user
- adds a new userPUT /api/v1/user/:id
- return hard coded json response:{message: 'User item updated.'}
DELETE /api/v1/user/:id
- return hard coded json response:{message: 'User item deleted.'}
- Commit and push your branch changes to the remote repository.
- Merge the
Assignment2
branch to themain
branch and push the changes to the remote repository.