- Write an HTML form that can make POST/PUT requests
- Describe how inputs transform into a req.body object on the server side
- Use bodyParser to read parameters passed from forms via POST requests
- Build RESTful routes that are wired to the correct semantically named view
Before this lesson, students should already be able to:
- Identify HTTP verbs, with crud actions, and view layers
- Build routes
- Pass parameters via dynamic segments and query parameters
SEPARATION OF CONCERNS
- Without Separation of Concerns (SoC), it becomes incredibly hard to maintain or change an application.
- Large applications like Facebook and Google utilize SoC since they are always adding new features, changing existing features, and working in large teams.
- SoC decomposes apps into parts with minimal overlap of code.
- SoC allows people to work on individual pieces of the system in isolation, facilitate reusability, and enable everyone to better understand the system.
FORMS
- Forms gives users the ability to pass information from the client to the server. For example, a sign up form.
So far we've created routes in Express and passed parameters from the browser to our server. We've constructed our views and dynamically built our html using Handlebars.
The ultimate goal of this lesson is to make our own form that will give the user the ability to create new todos.
First, in following with the principles of SoC, we are going to take our 'todos' route and logic and put it in it's own separate controller. We are creating our first modular application.
So in your /labs/express
folder, you should see a starter application named starter-code-hbs
with a bunch of work done already to get us going.
🔵 I DO, THEN YOU DO: PART 1 - Starter Code Set-up
-
cd
intostarter-code-hbs
and run the commandnpm install
which will read thepackage.json
file and install all of the dependencies specified in anode_modules
folder. -
Then, add a "start" script to your
package.json
that has the valuenodemon server.js
. Runnpm start
to start our server.In
server.js
, notice that we have a/todos
route. It containsvar seededTodos
which is an array of 2 seeded todos:
/* INDEX */
app.get('/todos', function(req,res) {
var seededTodos = [
{
description: "get beer",
urgent: true
}, {
description: "dry cleaning",
urgent: false
}
];
res.render('/todos/index', {
todos: seededTodos
});
})
-
Walk back through yesterday's Express Router lesson and refactor this app with the following:
- A
controllers/todos.js
file - Move your
/todos
route to the file.
- A
**Set-up Solution**
These routes are all related to todos. Let's put them in a separate "controller" file and directory. Create a a "controllers" directory and create a file called todos.js
. This will be our todos controller file.
Now move the routes pertaining to todos into that todos.js file.
/* INDEX TODOS */
app.get('/todos', function(req,res) {
var seededTodos = [
{
description: "get juicer",
urgent: true
}, {
description: "dry cleaning",
urgent: false
}
];
res.render('todos/index', {
todos: seededTodos
});
});
Require express at the top of that todos.js
file
var express = require('express');
invoke the router
var router = express.Router();
Change app to router
/* INDEX TODOS */
router.get('/todos', function(req,res) {
...
});
At the bottom of the file, export the router
module.exports = router;
Now in your server.js
, require the controller file we created. Be sure to include ./ at the beginning of the path to the controller, so node knows that this is not an NPM package.
var todosController = require('./controllers/todos.js');
Use the controller for all routes that start with /todos
app.use('/todos', todosController);
Since we are specifying that the controller will be used for all routes starting with /todos
, we don't need to show /todos
in our actual routes within our controller file.
/* INDEX TODOS */
router.get('/', function(req,res) {
...
});
🔵 Ask yourself
- Why are we requiring the todos controller in the server.js?
- We put module.exports = router at the bottom of the todos.js file. What is this line doing here?
- Why can we use ‘/‘ instead of ‘/todos’ in the todos controller?
🔵 I DO, THEN YOU DO
In server.js
, we have a var seededTodos
which is an array of 2 seeded todos.
As our app grows in size and complexity, we'll want to move logic out of our server.js
file. One method for doing so is by using an object called module.exports
. This allows us to clean up and share code throughout our app through breaking our code into separate files.
module.exports
allows us to separate our js files by exposing their contents
as one global variable. The global variable isn't assigned until we require the
file.
- For example, let's remove the
seededTodos
data fromcontrollers/todos.js
to ourdata.js
file by adding it as a property tomodule.exports
like so:
module.exports = {
seededTodos: [
{
description: "get juice",
urgent: true
}, {
description: "dry cleaning",
urgent: false
}
]
};
Now our module.exports
will have a property of seededTodos
which is an Array of todo objects.
- In
controllers/todos.js
, instantiate a variable to grant access to module we've created:
var data = require('../data.js');
- Now, our variable
data
is ourmodule.exports
object. We can access ourseededTodos
via ourcontrollers/todos.js
file like so:
res.render('todos/index.hbs', {
todos: data.seededTodos
});
This is essentially like calling
module.exports.data
Refresh the page and you can see that nothing changed in our browser, really we just moved the functionality into a different file.
What advantages does that bring to us with regard to separation of concerns in MVC?
-
The code is more organized and easier to understand by having separate controllers for our routes.
-
By putting the data in a separate file, we can now utilize that data in different files without replicating code. This can be really useful down the road when we create large files with functions associated with a specific task.
You'll start to see the necessity of module.exports
once we add
models and more routes to our application. Keeping everything in server.js
will quickly become a headache.
In general, programming is very free form — there are many solutions and many possible and perfectly acceptable answers and means of solving problems. However, Express
is a framework and has opinions about how we should write our code. Similarly, over the last 20 years or so programmers have developed several patterns to help clarify the purpose and design of routes in the context of HTTP and databases.
Our key words here today are:
- RESTful routes
- CRUD
HTTP | CRUD | RESTful View names |
---|---|---|
GET | Read | show / index / new / edit |
POST | Create | no views rendered |
PUT/PATCH | Update | no views rendered |
DELETE | Delete | no views rendered |
We're going to follow a very specific pattern at first.
View | Purpose | Blog example | RESTful route example |
---|---|---|---|
show.hbs | Shows an individual resource | A page that shows a single blog post | localhost:3000/posts/1 |
index.hbs | Shows all instances of a resource | A page that shows all blog posts | localhost:3000/posts |
new.hbs | Displays a form to create a new resource (form makes a post request) | A page that lets you create a new blog post | localhost:3000/posts/new |
edit.hbs | Displays a form that lets you edit a resource (form makes a post request) | A page that lets you edit an already created blog post | localhost:3000/posts/1/edit |
Roughly, here's how the table above would look in our server.js
(this generic example is BEFORE DRY-ing up our code with express.Router and a separate controller file.):
app.get('/posts', function(req, res) {
console.log("index");
res.render("/posts/index");
});
app.get('/posts/new', function(req, res) {
console.log("new");
res.render('/posts/new');
});
app.get('/posts/:id', function(req, res) {
console.log("show");
res.render("/posts/show");
});
app.post('/posts', function(req, res) {
console.log("create");
res.send("CREATE");
});
app.get('/posts/:id/edit', function(req, res) {
console.log("edit");
res.render("/posts/edit");
});
app.put('/posts/:id', function(req, res) {
console.log("update");
res.send("UPDATE");
});
app.delete('/posts/:id', function(req, res) {
console.log("delete");
res.send("DELETE");
});
**Nested Resources**
A restful architecture specifies the path to be constructed as a resource, the unique identifier of that resource and then another resource.
That second resource might be another object like a blog post or it might be a page.
So if we had users in our blog example above and users had blog posts and we wanted to see all of the blog posts of a specific user our route would like as follows:
localhost:3000/users/1/posts
| /users/:id/posts
| views/posts/index.hbs
(file path)
Don't worry about this idea of nested resources for now, but know this is where we're moving toward.
Convention dictates that the show
route returns one instance of a given resource.
🔵 Review the attached code
In previous lessons we learned about 'wildcard params'. (e.g. - http://localhost:3000/:id
)
Can someone remind me what a wildcard is and how we use it?
-
Create a SHOW route for a single Todo
-
Create a show.hbs view
-
Render a single todo from our
data.seededTodos
array.
For example:
🔵 Ask yourself
- How is the line
var todo = data.seededTodos[req.params.id];
grabbing a single todo? - What does this line allow us to do in our show.hbs view?
todo: todo
**SOLUTION**
Route/* SHOW TODO */
router.get('/:id', function(req,res) {
var todo = data.seededTodos[req.params.id];
res.render('todos/show', {
todo: todo
});
});
Sample views/todos/show.hbs
template
<h1>Todo - {{todo.description}}</h1>
<p>Urgent: {{todo.urgent}}</p>
Forms take user input and save it to a database, so let's start with a form to make a new todo item, as we need todos in our database.
Based on our table earlier. What view name do we want to use to create a page that has a form that will allow us to make a new resource?
-
Let's create a view for our form:
touch views/todos/new.hbs
-
Inside of
/todos/new.hbs
add the html form tags:
<form>
</form>
Now, forms need to know where to send information and the HTTP verb they'll be using to submit a request. Which verb do we use to create a new resource?
POST
What's the route we need to POST
to?
http://localhost:3000/todos
All forms must have a method
and an action
.
- Add method and action attributes to our form. The value of method will be
POST
and the value of action will/todos
<form method="POST" action="/todos">
</form>
-
Why can we use the /todos route for the form when the /todos route is already used to lead us to the index page?
Now we've got a form that's going to
POST
to/todos
– it'll hit our server, find an action with that combination of URL & verb, and run that code.There are no input fields or a submit button, but we'll add that next.
Luckily, form fields aren't that different from something we've seen before in JS – an object. In an object there are keys & values. Each input in a form is a key/value pair. Let's see a semi-complete example & dissect it.
<form method="POST" action="/todos">
<label for="description">description:</label>
<input name="description">
<label for="urgent">urgent: </label>
<span>yes</span><input type='radio' name='urgent' value="true" checked>
<span>no</span><input type='radio' name='urgent' value="false"/>
</form>
The name
attribute contains our key. What the user types into the text field or selects as a radio button is our value
. This would get parsed server side as:
req.body = {
description: 'whatever the user wrote in the input box',
urgent: 'true'
}
So if you did send that over, how would you access the entire todo object (i.e.- the form body itself)?
req.body
How would you access the todo's description?
req.body.description
To submit a form we need a submit button. But you know the designer's rule – never submit, always something specific. Plus, let's encapsulate our label/input combos with some divs
to get some grouping going on.
<form action="/todos" method="POST">
<div>
<label for="description">description:</label>
<input type="text" name="description">
</div>
<div>
<label for="urgent">urgent:</label>
<span>yes</span><input type="radio" name="urgent" value="true" checked>
<span>no</span><input type="radio" name="urgent" value="false">
</div>
<div>
<input type="submit" value="Add a New Todo">
</div>
</form>
Boom! Form, done.
Some of the controllers/todos.js
(specifically an INDEX route to show all of our todos, which we're storing in an array and our server/express configuration) is already written for you in the starter-code
, we should walk through it to reiterate how it works.
// controllers/todos.js
// This pulls in the module.exports object from
// data.js and assigns it to var data
// specifically it contains our seeded todos
var data = require('../data.js');
// Index shows all of a specific resource
// INDEX TODOS - returns All todos
router.get('/', function(req,res) {
res.render('todos/index.hbs', {
todos: data.seededTodos
});
});
Remember, the new action is just there to render the form to allow us to create a new Todo. The form is the part saying "when you hit enter/hit my button, I'm going to POST to /todos
!"
NOTE: If you have errors, be mindful of our discussion about why order matters when creating Express routes.
New Todo Route
/* NEW TODO */
router.get('/new', function(req, res){
res.render('todos/new');
});
How are feeling about your understanding of the lesson so far?
- 1: Things are a bit shaky at the moment.
- 2: Not sure about a few things, but I'm understanding the overall concepts.
- 3: I'm feeling pretty good.
Get in groups of 3. Answer your question first, then work on the others. We'll come back and each group will explain the answer to your assigned question.
- What does
CRUD
stand for? What are the 4 HTTP verbs that we're using to accomplish this? - What does REST stand for? What does it mean for our app to be stateless? How many REST-ful routes are in a conventional CRUD application?
- Which REST-ful routes have views associated with them? What do they all have in common?
- Assume an
actor
resource and aviews/actors/
directory. Create an INDEX route. The response object will return a view containing{ data: actors }
. - Assume an
actor
resource and aviews/actors/
directory. Create a NEW route. The response will return a view. - Create a form to create a new instance of an
actor
. The form will containname
andmostRecentFilm
fields - Explain
module.exports
and why we use it. - Explain
require()
and why we use it.
Awesome so now we have a form that renders on the page. So let's engage in some Error driven development.
- What happens when we add data and click the submit button?
- What's the difference between the 'new' route and 'create' route in terms of functionality?
🔵 Review the attached code
Build a todo CREATE route using router.post
. Don't worry about accessing the params from the form yet. Just send a response back ("Create a new todo is working!") to the client so you can verify the route works.
Create Todo Route Solution
/* CREATE TODO */
router.post('/', function(req, res){
res.send("Create a new todo is working!");
});
So what happens when we hit submit? The request is made and we can see that the correct route is getting hit.
Up until now, how have we passed data/information from the client to server?
Forms send data inside of what we call the body of a request (rather than inside params
or queries
).
GET requests only send headers.
POST requests will send a header, and the parameters get sent in the body.
By default, Express doesn't have the ability to read the body sent by POST requests so we need to npm install
another package.
body-parser
(https://github.com/expressjs/body-parser), is a library that parses the body of incoming POST requests and stores the data as a key value pair on our req
argument.
We access it via the body
property.
Remember this npm command will fetch the module in question, install it on our server, and load it into our package.json
- Then inside our
server.js
we need torequire()
the module and configure it.
var bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({
extended: true
}));
The params passed with a request will be "decoded" automatically, allowing you to use dot notation when working with JavaScript objects! The extended: true
makes the objects more JSON-like.
- In our
.post
route, changeres.send("Create working");
tores.send(req.body);
Now what happens when we submit a form?
🔵 Ask Yourself
- Why are we using bodyParser?
🔵 YOU DO
Install body-parser
in your app.
You might have noticed that we just used a slightly different method than we have in the past (.use
). We've used app.listen()
and app.set()
so far and now we've used app.use(bodyParser...
🔵 YOU DO
Take ten minutes to read about middleware http://expressjs.com/en/guide/using-middleware.html
🔵 Ask yourself
- What kind of middleware is body-parser?
Middleware is simply code that can be executed anywhere between a request and a response.
In our app we are logging out the server port once it has started - that is it. We get no other information about requests or errors. This is where Middleware comes in.
Body-parser is third-party middleware. It will:
- add a
body
property to ourreq
object - parse the body of the incoming request, if there is one
- add the data as the value of
req.body
Now the form data is accessible via req.body
. Our next step is to add our todo to the array.
Todo create route Solution
/* CREATE TODO */
router.post('/', function(req, res){
var newTodo = {
description: req.body.description,
urgent: req.body.urgent
};
data.seededTodos.push(newTodo);
// res.send("Create a new todo is working!");
res.redirect('/todos');
});
Why do we need to redirect after a .post
action?
**RECAP: ClientSide**
Data is sent from the client to the server, (from our browser to our express app). In the browser, we use HTML forms as an easy, user-friendly way to set up the write structure for muggles using our website.
This way our users can create new todos and update our database without any knowledge of http, routes, javascript, arrays or any of it really! The submit button is a one stop shop that as we've now learned triggers a whole series of processes.
It's just an html element that allows us to define how we want the data to be sent. The various attributes we configure let us set where the data is sent when the button is triggered. Remember how in our last unit we wanted to preventDefault behavior when the button was clicked? No longer, let that default behavior run free!
The action attribute is essential, it configures what route to send the data in the form to. In our example above that's to /todos
but could be anything we want. if we had a song storage app that route might be /songs
, if we had a ufo sightings database it might be /sightings
The method attribute refers to which HTTP verb we want to use. As you'll see tomorrow this can get a little hairy with PUT and DELETE, but today we're just specifying that it needs to be a POST request. Why is it not essential like the action attribute is? Because the form will always default to making a GET request to whatever route is set in the action attribute.
You might remember that we sent all kinds of data to the server on Tuesday using our :dynamic_segments and query parameters using GET requests. So why don't we just use that method to add ToDos? Semantics.
A GET request, is specifically used by the browser to ask for some resource, an image, a song, a todo, etc, and use the data appended in the url to augment that request.
POST requests are specifically used to create a resource on the server.
POST requests send their data in the body, hidden from the user
The http request looks like this:
POST / HTTP/1.1
Host: /todos
Content-Type: application/x-www-form-urlencoded
Content-Length: 36
description=take%20out%20the%20trash
**RECAP: ServerSide**
Routes are defined in server.js and when the server is running, they listen for incoming HTTP requests on the specified PORT
the basic route syntax looks as follows:
app.method('/route', function(req, res) {
// the code that needs to run when this route is called
})
The req
argument is an object that contains information about the incoming HTTP request:
- req.route
- req.query
- req.params
- req.body (see bodyParser below)
the res
argument is another object that contains information about the response we want to send to the server.
We initially started with using res.send to just send text to the browser, when using hbs however, we can use res.render to render an html/hbs file.
We can also use res.redirect to trigger another route before sending a response back to the browser
Express is a very lightweight framework and doesn't have the functionality to read data sent in the body of POST requests without including this module of code.
Configuring bodyParser on our server adds the body
property to our req
object so that we can easily access parameters sent by POST requests
you configure it as follows after installing the module to our application
$ npm install body-parser --save
var bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: false }));
Bit further ahead: module.exports vs exports
- Create a Delete Route
- Make the index page send a DELETE request
- Create an edit route
- Create a link to the edit route
- Create an update route
- Make the edit page send a PUT request
CRUD
HTTP
MVC
Acronyms galore. But let's quickly revisit what these are, and how we have applied them thus far.
RESTful Routes:
URL | HTTP Verb | Action |
---|---|---|
/photos/ | GET | index |
/photos/new | GET | new |
/photos | POST | create |
/photos/:id | GET | show |
/photos/:id/edit | GET | edit |
/photos/:id | PATCH/PUT | update |
/photos/:id | DELETE | destroy |
-
controller files (plural)
-
routes (plural or singular depending)
-
views dir (plural)
-
view files (index, new, show, edit)
Inside of our controllers/todos_controller.js
file, add a DELETE route:
router.delete('/:id', function(req, res) {
data.seededTodos.splice(req.params.id, 1); // remove the item from the array
res.redirect('/todos'); // redirect back to the index route
});
In our controllers/todos_controller.js
file, make sure that your index route looks something like this:
router.get('/', function(req, res) {
res.render('todos/index', {
todos: data.seededTodos
});
});
Inside of our todos/index.hbs
file, add a form with just a delete button.
<h1> Todos </h1>
<ul>
{{#each todos}}
<li>Description: {{description}}</li>
<li>Urgent: {{urgent}}</li>
<br>
<form action="/todos/{{@index}}">
<input type="submit" value="DELETE"/>
</form>
{{/each}}
</ul>
When we click "DELETE" on our index page (index.hbs), the form needs to make a DELETE request to our DELETE route.
Click the button and watch your server logs in the terminal. Where did the request go?
The problem is that forms can't make DELETE requests. Only POST and GET requests. There is a way around this fact. First, we need to install an npm package called method-override
.
npm install method-override --save
Now, in our server.js
file, add:
// include the method-override package
var methodOverride = require('method-override');
//...
// after the app has been defined
// use methodOverride. We will be adding a query parameter to our delete form named _method
app.use(methodOverride('_method'));
Now, go back and set up the delete form to send a DELETE request to the appropriate route
<form action="/todos/{{@index}}?_method=DELETE" method="POST">
Several reasons:
-
When HTML and HTTP were first specked out there wasn't a real user story around Updating and Deleting, or even Posting content. Updates and deletions were made by the authors of the content, not by external users. Initially, HTTP was simply a GET method.
-
As the internet developed into something closer to what we understand it to be now, HTTP added PUT and DELETE methods in 1997 (HTTP/1.1). HTML was not updated to handle them, so developers created workarounds to allow web users to still make PUT and DELETE requests using POST as the thoroughfare.
-
These workarounds are now so ubiquitous (often included by default in frameworks) that providing a solution in HTML has become redundant.
You can read the conversation about adding PUT and DELETE form functionality to HTML5 here
- A more technical reason does exist, if it gives you any solace.
- A PUT request wouldn't look any different than a post, the data still needs to be sent in the body vs. the header. Therefore, the server would still need some kind of flag to differentiate between a PUT and POST request, otherwise the requests look the same and provide a similar output.
- A DELETE request, doesn't have any data or content in the body. Forms add data to the body of an HTTP request, so making an HTML form have DELETE functionality also doesn't really make sense.
In our controllers/todos_controller.js
, create a GET route that will display an edit form for a single todo item.
router.get('/:id/edit', function(req, res){
res.render('todos/edit', {
todo: {
id: req.params.id
description: data.seededTodos[req.params.id].description,
urgent: data.seededTodos[req.params.id].urgent,
}
});
});
Now create an edit.hbs file in your views folder, and then create a very basic form for editing in the views/edit.hbs
.
<h1>Edit {{todo.description}}</h1>
<div class="row">
<form class="col-md-6 col-md-offset-3">
<div class="form-group">
<label for="description">description:</label>
<input type="text" name="description" class="form-control" value="{{todo.description}}">
</div>
<div class="form-group">
<label for="location">location:</label>
<input type="text" name="location" class="form-control" value="{{todo.location}}">
</div>
<div class="form-group">
<label for="urgent">urgent:</label>
<input type="text" name="urgent" class="form-control" value="{{todo.urgent}}" />
</div>
<div>
<input type="submit" value="Update Todo" class="btn btn-default">
</div>
</form>
</div>
Inside our index.hbs
file, add a link to our edit route that passes in the index/id of that item in the url
<h1> Todos </h1>
<ul>
{{#each todos}}
<li>Description: {{description}}</li>
<li>Urgent: {{urgent}}</li>
<br>
<div><a href="/todos/{{@index}}/edit">Edit</a></div>
<form action="/todos/{{@index}}">
<input type="submit" value="DELETE"/>
</form>
<hr>
{{/each}}
</ul>
In order to UPDATE an item, we use the HTTP verb PUT.
Inside the todos_controller.js
add the following:
router.put('/:id', function(req, res) {
var todoToEdit = data.seededTodos[req.params.id];
todoToEdit.description = req.body.description;
todoToEdit.urgent = req.body.urgent;
res.redirect('/todos');
})
Check the server logs. What happens when we submit our changes?
When we click on the "Submit Changes" button on our edit page (edit.hbs), the form needs to make a PUT request to our update route.
<div class="row">
<form action="/todos/{{todo.id}}" method="" class="col-md-6 col-md-offset-3">
<div class="form-group">
<label for="description">description:</label>
<input type="text" name="description" class="form-control" value="{{todo.description}}">
</div>
<div class="form-group">
<label for="location">location:</label>
<input type="text" name="location" class="form-control" value="{{todo.location}}">
</div>
<div class="form-group">
<label for="urgent">location:</label>
<input type="text" name="urgent" class="form-control" value="{{todo.urgent}}" />
</div>
<div>
<input type="submit" value="Update Todo" class="btn btn-default">
</div>
</form>
</div>
The problem is that forms can't make PUT requests. Only POST and GET requests. So we need to use method-override again.
Now go back and set up our edit form to send a PUT request.
<form action="/todos/{{todo.id}}?_method=PUT" method="POST" class="col-md-6 col-md-offset-3">
Now that we see from the attached example how we can work with the Views and change data in a static array we used as seed data. Now we will move on to implimenting the model and making sure our controller works with our model and our live database. The full CRUD operations will be performed on the model. The views will be rendered using HBS and an additional method-modifier package so as we can use forms to make Delete and Put requests which are not the defaults for the form operations.