- Identify how an Express app fits within the MVC framework
- Connect an Express app to a MongoDB database
- Implement CRUD functionality in an Express app using Mongoose
So far in this unit you've learned about a number of tools - Node, Express, MongoDB and Mongoose - that developers can use to build a server-side Javascript application. You have yet, however, to use them all together. We'll be spending the bulk of today's lesson connecting everything and creating an application that can receive HTTP requests, retrieve data/make changes to a database and send information back to the end-user.
Today we'll be working on a brand new app called "When President." It's a simple, one-model CRUD app that allows a user to declare who they're voting for as president in a given year.
Let's begin by cloning down the repo...
$ git clone https://github.com/ga-wdi-exercises/whenpresident.git
The starter and solution code are branches in the
WhenPresident
repo.
Checkout to the proper branch...
$ git checkout express-mongoose-starter
Optionally, create a new branch on which you will do your work...
$ git checkout -b MyName-express-mongoose
Install the modules listed in package.json
...
$ npm install
Then, start the Mongo server in a separate Terminal tab or window...
$ mongod
Check out what the starter code looks like in the browser by running $ nodemon
and then visiting http://localhost:3001
in the browser.
Take 10 minutes to review the Express application as it stands. As you're going through it, do the following...
As you're reviewing the app, try to fill in the blanks in the below Rails-to-Express MVC table. Your job here is to find the ME(A)N equivalents of components in a Rails application. Keep in mind, the answers for many of these are portions of files rather than entire files themselves.
Rails | Express | |
---|---|---|
Language | Ruby | |
Database | PostgreSQL | |
ORM | Active Record | |
Database (Config) | database.yml |
|
Route | routes.rb |
|
Controller | candidates_controller.rb |
|
Model | candidate.rb |
|
View | index.html.erb |
.
You are welcome to code along during the "I Do's" and "We Do's" in this lesson plan. We do ask, however, that if you fall behind, do not attempt to catch up during those sections. Instead, tilt down your screen and watch / take notes.
You are more than welcome to catch up when we get to the "You Do's," during which the instructors are available to help.
Like ActiveRecord for Rails, Mongoose is an ODM we can use to represent data from a Mongo database as models in a Javascript back-end.
In order for us to use Mongoose to communicate with our database, we need to link it up to our Express application. We'll do this by...
- Establishing a connection with a Mongo database.
- Define a Mongoose schema and model.
- Generate some seed data.
- Install Mongoose via the command line:
npm install --save mongoose
. - In
connection.js
, require "mongoose" and save it to amongoose
variable. - Define a
CandidateSchema
using mongoose's.Schema()
method. - Define a "Candidate" model built off
CandidateSchema
withmongoose.model()
. - Connect to our
whenpresident
database usingmongoose.connect()
.
What argument do we pass into `mongoose.connect()`?
The location of the Mongo database.
Does `.Schema()` modify our database? What about `.model()`?
No. It's only once we start querying the database that it changes.
What does `module.exports` do?
It allows us to export code from one file to another. We could export only portions of the code or all of it if we wanted to.
var mongoose = require("mongoose")
- In order to reference Mongoose, we need to require its corresponding node module and save it in a variable we can reference later.
mongoose.Schema( )
- We use Mongoose's schema method to define a blueprint for our Candidate model (i.e., what attributes it will have and what data types they will be).
mongoose.model( )
- We attach our schema to our model by passing in two arguments to this method: (1) the desired name of our model ("Candidate") and (2) the existing schema.
mongoose.connect
- We also need to link Mongoose to ourwhenpresident
Mongo database.
Mongoose is now connected to our Express application. Let's seed some data into our database using Mongoose.
In connection.js
we need to...
- Remove any references to seed data from
connection.js
. - Set
module.exports = mongoose
.
Now, create a new db/seed.js
file. In it we will...
- Require
connection.js
andseeds.json
, saving them to their ownmongoose
andseedData
variables respectively. - Define a
Candidate
variable that will contain our Mongoose model definition. - Write Mongoose queries that accomplish the following...
- Clears the database of all data, and
.then
... - Inserts our seed data into the database, and.then
... - Callsprocess.exit()
.
We can test this by...
- Running
$ node db/seed.js
in the Terminal. - Then run
$ mongo
in the Terminal and enter the following commands via the Mongo CLI interface...
```
> use whenpresident
> db.candidates.find()
```
If you need help remembering Mongoose queries, the official documentation is a good place to look.
Why are we able to write out `mongoose.model("Candidate")` in `seed.js`?
Because we defined that candidate in
connection.js
, which has been required.
What does it mean to pass `{}` as an argument into `.remove()`?
To remove everything. An empty object means that we're not going to restrict removal to certain key-value pairs.
Notice that
connection.js
no longer contains any reference to seed data. It now only serves as a connection between our application and database.
var mongoose = require("./connection")
- Note that this timevar mongoose
is not set torequire("mongoose")
. Instead, it represents a connection to our database.
var Candidate = mongoose.model("Candidate")
- Because we defined our model inconnection.js
, we can reference it like so.
Candidate.remove({})
- This clears our entire database. We're not passing in any parameters, so Mongoose interprets this command as delete anything!
Candidate.collection.insert(seedData)
- Create a collection using the JSON contained in our seed file.
First order of business: display all candidates stored in the database. We'll do this by adding code to the controller that...
- Retrieves all of the candidates from the database.
- Renders a view displaying the retrieved candidates.
In index.js
, let's make some changes to our variable definitions...
- Rename
db
tomongoose
. We will be calling Mongoose methods on this variable - this makes more sense semantically! - Define a
Candidate
model in the exact same way as inseed.js
.
Now let's move down to our index route...
- Use Mongoose to retrieve all Candidates from our database, and
.then
... - Render our existing index view, making sure to set
candidates
(the variable we will be accessing in the view) to the response of our Mongoose method.
What does `res.render` mean? What do we need to pass into it as arguments?
res.render
is used to render the server response back to the browser. In this example we pass it (a) the view we want to render and (b) the data that should be available to it (i.e., candidates).
What does it mean to pass `{}` as an argument into `.find()`?
Like
.remove
, it means to find everything. The search is not limited to certain key-value pairs.
Why does our `res.render` statement need to be wrapped in a callback?
We want to wait until the Mongoose query has been completed before we render anything, especially if the render is dependent on data returned from the database.
Candidate.find({})
- Retrieves all candidates in the database since we are not passing in any parameters to the method.
.then(function(candidates){ ... })
-candidates
represents the all the Candidates pulled from the database. We can then reference this inside of.then
.
candidates: candidates
- A little confusing, but thecandidates
we will be referencing in our view are now set to thecandidates
that are returned by Mongoose.
So we can show all candidates. You know what's cooler than all candidates? ONE candidate. Let's go back into the controller and creating a corresponding route / action for that.
Let's make changes to our existing show route...
- Use a Mongoose method to retrieve the candidates whose name is located in the browser URL. (Hint: use
req.params
)..then
... - Render the existing show view, making sure to pass in the retrieved candidate as the value to the
candidate
key.
What's the difference between `.find` and `.findOne`?
.find
returns multiple items..findOne
only returns one.
In NodeJS, in order to process user input received through a form we will need to install and implement the body-parser
middleware.
Install it via the command line -- npm install --save body-parser
-- then make the following changes to your index.js
file...
var parser = require("body-parser")
- Requirebody-parser
so we can reference it later.
app.use(parser.urlencoded({extended: true}))
- configure the parser to support html forms
Let's create a new candidate form. We'll add it to our existing index view...
What did we use in Rails to create an input form?
form_for
helper.
form_for @candidate do |f|
f.input :name
f.input :year
f.submit
end
What attributes are important for a form tag? Why?
action
andmethod
. This defines what route we will submit the form contents to.
What params do we need to access in the route/controller?
{ candidate: { name: "Al Gore", year: 2000 }
What must be be in the form tag to create those params?
Input tags will contain
name="candidate[year]"
.
Create a form in the index view.
- Using the
action
attribute, the form should direct to/candidates
. - Using the
name
attribute, each input should store the value of a candidate attribute (i.e.,name
andyear
) to a key that exists inside of acandidate
object.
Why do we set the `name` attribute to something like `candidate[name]`? How does this impact how we access this information in `index.js`?
All candidate information will be available to us inside of a
candidate
object on the back-end.
Before we actually create a new candidate in the database, let's make sure we can access the user input submitted through the form.
- In
index.js
, create an expressPOST
route that corresponds with/candidates
. - The route's only content should be a
res.json()
statement that returns the user input. (Hint: this is stored somewhere inreq
).
How are the form and `req.body` related?
The values a user submits through the form can be found in
req.body
on the back-end.
What does `res.json` do?
It's sends a response back to the browser in JSON form. This functions similarly to
format.json
in Rails.
Why are we accessing `req.body` instead of `res.body`?
Because we want to render whatever the user sent through the form as JSON.
Click to reveal...
res.json(req.body)
- The server will respond with JSON that contains the user input, which is stored inreq.body
. This should look just like the output of Rails APIs you have created in this course.
Let's modify this post route so that it creates a candidate in our database.
- In
index.js
, use a Mongoose method to create a new candidate. Pass in an argument that contains only the candidate's name. (Hint: Again, this is stored somewhere inreq
)..then
... - Redirect the user to the show view for the newly-created candidate.
What is `res.redirect`? How is it different from `res.send`, `res.render` and `res.json`?
res.redirect
initializes a new request-response cycle and usually is not passed any data from the controller.
Candidate.create(req.body.candidate)
- Pass in the name stored inreq.body
as an argument to.create
.
res.redirect()
- Redirect the user to the new candidate's show view. In the callback,candidate
represents the new candidate in our database.
Onto editing and updating candidates. We'll set up a form in our show view to allow users to submit updated candidate information.
Create an edit form in the show view.
- Using the
action
attribute, the form'saction
attribute should direct to our application's show URL. - Using the
name
attribute, each input should store the value of a candidate attribute (i.e.,name
andyear
) to a key that exists inside of acandidate
object.
Why does `method="post"` even though we are updating (vs. creating) something?
HTML does not support
PUT
orPATCH
. That being said, we can make aPOST
request and define behavior on the back-end that will actual update something instead of create.
Click to reveal...
method="post"
- Wait, why is this aPOST
method? Aren't we supposed to send aPUT
orPATCH
request?
- In
index.js
, create a.post
route inindex.js
that corresponds to our new form. - In it, use a Mongoose method to find and update the candidate in question. (Hint: Refer to the Mongoose lesson plan or documentation).
.then
, redirect the user to the updated candidate's show page.
How come `.findOneAndUpdate` has 3 arguments while `.create` has only 2?
Because we need to identify the thing we are updating AND what it's going to be updated with.
Click to reveal...
.findOneAndUpdate()
- This method takes three arguments: (1) the new params, (2) the candidate to be updated and (3)new: true
, which causes the modified candidate to be returned in the callback.
Note: this screenshot cut off a method call, can you think what should happen after the update?
We may not get to this during the lesson, but you should be able to implement this yourselves with the instructions below.
We're almost there! Last bit of CRUD functionality we need to implement is DELETE
. Let's start by adding a delete button to our show view...
- Using the
action
attribute, the form should direct to/candidates/:name/delete
.
Why can't we use `app.delete` for a `DELETE` route?
Again, because HTML only supports
GET
andPOST
, notPUT
PATCH
orDELETE
.
- In
index.js
, create a route that corresponds to our delete button. - In it, use Mongoose to find and delete the candidate in question. (Hint: Refer to the Mongoose lesson plan or documentation).
- What does module.exports do?
- Why does method="post" even though we are updating (vs. creating) something?
- Why do we use promises and callbacks when calling methods on a Mongoose model?
You should now be able to complete the second part of the YUM homework.