Resources, Routing, and MVC
- Many web apps (especially those that are “server-side” or “back-end”)
are designed around the idea of “Resources” (Think “nouns” from OO design.)
- Book
- Car
- Student
- User
- Order
- Ticket
Model
- Each Resource is typically described as an object that we call the “model”
- This model has the basic properties as instance variables.
- e.g. a Toy might have a name, description, price, and manufacturer
- e.g., a User might have a first name, last name, password, and an “admin” flag.
- Often these correspond directly to the columns in the corresponding database table.
- We separate the model from the rest of the code because the model’s implementation does not depend on the framework (Express, Rails, etc): In theory, you could change frameworks without modifying models.
- The model is primarily a description of an individual resource; but, it may also contain some “business logic” (e.g., validation rules.)
- Other business logic goes in different classes in the Model section
ToyAnalyzer
(e.g., collect statistics about toys)- Rules about who is allowed to own/buy a certain toy (e.g., must be 12 to own a BB gun.)
- DB access code. (Sometimes integrated into the Resource model, sometimes separate.)
Operations and routes
- Every resource has four basic operations:
- Create
- Read
- Update
- Delete
- (affectionately called CRUD)
- Each operation needs a unique URL/Route.
- These routes can be arbitrary, but they tend to follow a pattern:
GET /toy
<— list (read) all toysGET /toy/:id
<— show (read) the toy with the given idPOST /toy
<— create a new toyPOST /toy/:id
<— edit (update) an existing toy.GET /toy/new
<— Display the form to create a new toyGET /toy/:id/edit
<— Display the form to edit a toy.DELETE
is interesting. We’ll talk about it later.
- Notice how you could write a simple program to create these routes given the name of a resource. (Most frameworks have such a feature.)
- Now, we could just stuff all the necessary code into
index.js
; but that wouldn’t be good design.- It would get messy (wouldn’t scale well)
- It would mix “framework” code with “business code” (e.g., the model)
/* Display all toys (read) */
app.get('/toys', (req, res) => {
// ...
})
/* Create a new toy */
app.post('/toys', (req, res) => {
// ...
})
/* Display a form to create a new toy */
app.get('/toys/new', (req, res) => {
// ...
})
/* Display (read) details for one toy.
:id represents a "route parameter" */
app.get('/toys/:id', (req, res) => {
// ...
})
/* Edit(update) a toy */
app.post('/toys/:id', (req, res) => {
// ..
})
/* Display a form to edit (update) a toy */
app.get('/toys/:id/edit', (req, res) => {
// ...
})
Controller
- The code that “glues” the route to the appropriate logic is called the “Controller”
- It “controls” the flow of the program
- In many frameworks, the Controller is a class.
- The method name for each operation is somewhat standard:
GET /toy
—>index
—> list (read) all toysGET /toy/:id
—>show
–> list (read) one toyPOST /toy
—>create
–> create a new toyPOST /toy/:id
—>update
–> update a toy.GET /toy/new
—>newToy
–> Send new toy formGET /toy/:id/new
—>edit
–> Send edit toy form
/* Display all toys */
app.get('/toys', (req, res) => {
toyController.index(req, res)
})
/* Create a new toy */
app.post('/toys', (req, res) => {
toyController.create(req, res)
})
/* Display a form to create a new toy */
app.get('/toys/new', (req, res) => {
toyController.newToy(req, res)
})
/* Display details for one toy.
:id represents a "route parameter" */
app.get('/toys/:id', (req, res) => {
toyController.show(req, res)
})
/* Edit a toy */
app.get('/toys/:id/edit', (req, res) => {
toyController.edit(req, res)
})
/* Update a toy */
app.post('/toys/:id', (req, res) => {
toyController.update(req, res)
})
-
Notice that you can write a function that will automatically create the code above given (a) the name of the resource, and (b) a Controller object.
-
The main job of the controller is to
- Call the correct model method(s)
- Render the correct view.
View
- In “server-side” app, the view is the template code that generates the web page that the user sees.
- In this context, views are separate because they aren’t usually “plain” code (e.g., just JavaScript, just Ruby, etc.). They are templates that mix HTML with code in some way.
Database
- There are many databases and many ways of connecting to a DB. I’m going to
punt on this for now, and just treat the
ToyDB
object as a “black box” that we call methods on.- (I personally don’t like “magic” stuff when I teach, but you’ll see why I put this off.)
Putting it all Together
- walk through showing the list of toys.
- Show that
toy
in .ejs is an Object with properties - Show how view uses .ejs to build links to edit and show routes.
- (Many frameworks have a library to automate this. Something like
route_for(toy)
- (Many frameworks have a library to automate this. Something like
- Show that
- walk through showing the details of one toy.
- Routes can have parameters (e.g., the
:id
)
- Routes can have parameters (e.g., the
- walk through the creation of a new toy.
- This requires the use of two routes / controller methods
- The first one displays the form,
- The second one responds to the form submission and creates the object.
- Notice that one .ejs file can include another.
Database
- There are many data storage options
- I will start with SQLite because it is file-based and doesn’t require the installation of any separate tools/processes (just an npm package).
- Regardless of what you choose, put your DB access in a separate class, so it can be easily swapped out
if you decide to change DB’s.
- Better yet, give these different DB classes identical interfaces so they are easy to swap out.
SQLite
- Basic idea:
- Create DB object from local file containing DB.
- Then just issue SQL commands
db = new sqlite3.Database(__dirname + '/toys.sqlite')
db.run('CREATE TABLE Toys (id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL, manufacturer TEXT NOT NULL, price REAL NOT NULL);')
- There are several methods for issuing SQL commands: https://github.com/TryGhost/node-sqlite3/wiki/API
run
: Good when you aren’t expecting results.get
: Good when you expect one row of results.all
: Good when you expect many rows of results.- There are others, but the three above work for me.
Callbacks / Promises
- The SQLite API is written to use callbacks.
- Rather than waiting for the DB operation to complete, you pass a lambda to the DB method that is executed when the operation completes.
- This is a new style of programming for many of you. It will take some getting used to.