REST API with CRUD using Express and Mongoose

thecodepixi

Emmy | Pixi

Posted on June 21, 2020

REST API with CRUD using Express and Mongoose

Previously in this series we walked through setting up your first Express server, and then setting up the beginnings of an Express API with Mongoose/MongoDB data storage.

This post will jump off exactly where we left of in the previous post, so if you haven't been following along, I recommend at least going back and reading through that post first.

If you prefer to jump straight into the code, you can find that here

And if you prefer to learn by watching video tutorials, you can watch the live stream of this entire API build here

Getting Started...

In the last installment in this series, we left off with having just completed our first Mongoose Schema and database Model. This means we're ready to start building our router and our API endpoints!

The first thing we need to do is create a new folder in the top level directory of our project called routers and put a file inside that folder called puppy.js

$ mkdir routers
$ touch puppy.js
Enter fullscreen mode Exit fullscreen mode

Then we want to tell our Express server (in index.js) that we want to use(puppy.js) for our Puppy routing endpoints.

Here's how we'll do this:

// index.js

const puppyRouter = require("./routers/puppy")
app.use("/puppies", puppyRouter)
Enter fullscreen mode Exit fullscreen mode

these new lines of code can go directly beneath your Mongoose connection function call

What the above is doing is telling our Express server that for any endpoint starting with /puppies we want it to use the routing specified in our puppy.js file.

Now What?

So we've told our Express server to use the puppy router. Now what do we put in there to make this work?

To start writing our routes, we first need to open up our puppy.js file and add some requirements.

First, in order to create our routes, we need to require the Express Router:

//puppy.js
const router = require("express").Router()
Enter fullscreen mode Exit fullscreen mode

This router constant will be used to call all of our routing functions.

Then we also need to require the Puppy model we previously created, so that we can use our model to acces and make changes to data in our MongoDB database.

//puppy.js
const router = require("express").Router()
const Puppy = require("../models/Puppy.model")
Enter fullscreen mode Exit fullscreen mode

Creating a route in Express is as easy as using the correct HTTP verb function (ie: get, post, etc) and pass in the route we want to use to preform that action.

the rest of the code in this tutorial is going to be written inside of puppy.js

Reading from our MongoDB Collection

We're going to start off easy and write a route to access all of our puppies!

a cute little row of 101 dalmations

To do this, we're going to access our Puppy model using some built-in Mongoose functions. Since our const Puppy points to a Mongoose model, we don't also need to require(mongoose) here, which I think is a nice perk!

First, we need to set up an Express get request:

router.get("/", (req, res) => {
  res.send("Gimme the puppies!")
})
Enter fullscreen mode Exit fullscreen mode

If you go ahead and start your server (using the dev script we added in a previous lesson) and navigate to localhost:5000/puppies/ you should see the string 'Gimme the puppies!'.

What's happening here is we are telling Express we want to get the / route, and then we pass a callback that includes the req (or request object), and the res (or response). Then we're telling Express the send the string "Gimme the puppies!" as our response.

Pretty cool, right!?

Now, let's get fancy

We're going to add onto this route and use the built in Mongoose method find to access every Puppy in our collection.

router.get("/", (req, res) => {
  Puppy.find()
    .then(puppies => res.json(puppies))
    .catch(err => res.status(400).json("Error: " + err))
})
Enter fullscreen mode Exit fullscreen mode

Above, we are using the the same get request frame, and now we're using the Mongoose find method to access all of the instances of Puppy in our collection, and send them back to the response as JSON.

Since find is an asynchronous function, we use then to access and take action on the data that gets returned to us, only once the data has been returned. We also include a catch just in case there's an error. In this case, if there's an error, we send back a 400 status code to indicate a server error, and send back the error details as JSON. (we will use this error format for every database request we write in this tutorial)

Now if you were to look at this route in your browser, you'll see nothing returned. And that's a good thing! We haven't added a Puppy to our collection yet, so there's nothing to be returned.

Let's fix that...

Updating Our Collection

Before we get started with sending our data to our database, we need a way to do that. My preferred method is a product called Postman. Postman allows you to manually test API endpoints by creating request bodies manually without needing an actual GUI form or interactive element to send your data with.

Go ahead and download Postman, and then come back for this next part, or if you have another API interaction tool you prefer feel free to use that!

Now that we have Postman, and are able to test our endpoints, we can go ahead and set up our first post route.

router.post("/", (req, res) => {
  const name = req.body.name
  const breed = req.body.breed
  const age = Number(req.body.age)
  const cute = req.body.cute
  const well_behaved = req.body.well_behaved
  const adopted = req.body.adopted

  const newPuppy = new Puppy({
    name,
    breed,
    age,
    cute,
    well_behaved,
    adopted,
  })

  newPuppy
    .save()
    .then(() => res.json("New puppy created!"))
    .catch(err => res.status(400).json("Error: " + err))
})
Enter fullscreen mode Exit fullscreen mode

This one's pretty long, so let's step through this code and talk about what's happening.

First, we set up our post request route, and pass in the root route (/). This will send the post request to /puppies/, which is the RESTful endpoint for adding new instances to our collection.

Then, we create some variables from the data passed in from our req.body, or the body of our request. When we test this endpoint, we're going to be passing in a set of key-value pairs where the key is the same as the data column we want to add to for this Puppy. If you know that your data will always been passed to the API in this format (via some sort of client-side validation, for example) you can alternatively pass the entire req.body to new Puppy().

We then take all of those variables and use them to create a new instance of our Puppy model. Once we have our instance of a Puppy, we can use the built in Mongoose function save to add this Puppy to our MongoDB collection. Just like the find method, the save method is asynchronous, so we will use then to send back our response. The save method does not return any data by default, so instead we'll just send back the string "New puppy created!" to indicate success here.

Now we can save our router file and test this out in Postman!

Example of a post route in Postman using Puppy data

In the example above, you can see that I've chosen the post verb option from the dropdown on the left-hand side, passed in our full URL (localhost:5000/puppies/) and then used the request body creator below to pass in my Puppy data as raw JSON. (Make sure to choose JSON from the dropdown!)

Here's the puppy I created:

//req.body
{
  "name": "Hank",
  "breed": "English Springer Spaniel",
  "age": 10,
  "cute": true,
  "well_behaved": false,
  "adopted": true
}
Enter fullscreen mode Exit fullscreen mode

Then we can click "Send" and the result panel at the bottom will read "New puppy created!"

An English Springer Spaniel being scrutinized from some very similar looking cows

Now if we create a new tab using the + symbol button at the top, we can create a get request to /puppies/ and see our new Puppy returned to us from the database!

Get request for all puppy data

Now we get back the puppy we created, and we can see that MongoDB automatically gave our instance of a Puppy an _id field populated with a unique ID code. We'll be using this _id field to get individual instances of a Puppy, as well as making updates via put request, and eventually, sadly, delete-ing puppies if necessary.

Reading Specific Data

Now that we know how to add puppies to our collection, it's understandable that we won't always want to get information about every single Puppy in our collection. Personally, I'm most interested in Hank, because he's my puppy. So how do I access only the data about him?

We can use a method that's very similar to the find method we used previously. Mongoose provides us with the method findById, and it does exactly what it says on the tin. We simply pass in the _id of the Puppy we want to find.

To get access to the _id information, we're going to use a route parameter by adding on to our base /puppies/ route. Here's the get request we'll be using:

router.get("/:id", (req, res) => {
  res.send(`The id you want to get is ${req.params.id}`)
})
Enter fullscreen mode Exit fullscreen mode

Using the above, if you navigate to localhost:5000/puppies/123, you should see the string "The id you want to get is 123" in your browser window.

We'll be using req.params.id to send a specific _id to MongoDB in order to access the specific Puppy we want.

router.get("/:id", (req, res) => {
  Puppy.findById(req.params.id)
    .then(puppy => res.json(puppy))
    .catch(err => res.status(400).json("Error: " + err))
})
Enter fullscreen mode Exit fullscreen mode

Now, we're using the aforementioned findById method, coupled with the id passed to us from req.params.id to request only that instance of a Puppy, and send the data back as JSON.

We can test this in Postman, using the _id that was assigned to the Puppy we previously created...

your _id will be different, as each _id is completely unique

Postman API call to our /puppies/:id route, using the ID from the previously created puppy

As you should see above, we make a get request to /puppies/:id, passing in the _id for our previously created Puppy in place of :id, and we get back that instance of Puppy.

If you want to test this out further, I encourage you to create a few more Puppy instances using our previously created post route, and then access each of them one at a time using this new route.

Updating Instances

Now that we're able to get data to and from our database, we also want to be able to make changes. In the event that a Puppy ages, gets adopted, or gets some training and becomes well behaved, we want to be able to update those details in our database.

This process is an amalgamation of what we've done so far. It's similar to get-ting and post-ing an individual instance of a Puppy. Remember how Mongoose gave us a convenient findById method? Mongoose also gives us a findOneAndUpdate method for finding and updating instances.

However, in order to use this function, we need to make an update to our mongoose.connect function in our index.js file.

// index.js
mongoose.connect(uri, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  useFindAndModify: false, // this is the new line of code we're adding
})
Enter fullscreen mode Exit fullscreen mode

In the above code block, passing another option to our connect function and telling Mongoose to set useFindAndModify to false. This will allow us to use the Mongoose findOneAndUpdate method. It's not important in this use case to understand why we're doing this, but if you want to learn more about this, you can check out the Mongoose documentation.

Now that we've got that sorted out, we can create our update route using the put http verb...

router.put("/:id/update", (req, res) => {
  let updates = req.body //we set a variable equal to the entire req.body

  Puppy.findOneAndUpdate({ _id: req.params.id }, updates, { new: true })
    .then(updatedPuppy => res.json(updatedPuppy))
    .catch(err => res.status(400).json("Error: " + err))
})
Enter fullscreen mode Exit fullscreen mode

In this scenario, we are assuming that the only data being passed in the req.body are the updates being made to this specific puppy. Otherwise, we would need to write some extra code to extract the specific updates being made.

findOneAndUpdate conveniently indentifies whether the instance being accessed already has the key (or multiple keys) being passed in by the update object. And since we created this model using a schema, we are only allowed to update columns that already exist. If we were to try to send an update that included a column that does not exist, no updates will be made to the database.

findOneAndUpdate takes in 2 required arguments, and one optional:
First we find the Puppy we're targeting by looking for a Puppy instance with a matching _id ({ _id: req.params.id }), then we pass in the updates variable as the second argument. We can also include { new: true } as an optional third argument, which will cause the updated Puppy data to be returned from the function.

We can test this new endpoint, again using Postman. Let's say I got my Puppy Hank some training lessons, and he's now perfectly well behaved. I can send an update to this endpoint, and in the request body test well_behaved to true:

Updating a puppy using our /:id/update endpoint

You should see above that I sent the request to this new endpoint that I described above, and in the reponse we see the updated Puppy instance for Hank, where well_behaved is now set to true.

Deleting Data

Sadly, the only thing left to do is to delete some of our puppies.

a puppy who is very sad that they are about to be deleted

Since you've been following along, I bet you can guess what http verb we need to use this time: delete!

Similarly to the built in findOneAndUpdate method, Mongoose also provides us with a convenient findByIdAndDelete method.

Here's how we'll utilize this method in our API:

router.delete("/:id", (req, res) => {
  Puppy.findByIdAndDelete(req.params.id)
    .then(() => res.json("Puppy deleted =( "))
    .catch(err => res.status(400).json("Error: " + err))
})
Enter fullscreen mode Exit fullscreen mode

This works pretty similarly to our findById method. We pass in the _id of our Puppy as a request param, and the method does the rest of the work for us. As with all of our other Mongoose methods, this is asynchronous, and like the save method does not return any data to us. Instead we send back the string "Puppy deleted =(" to indicate successful deletion.

Test this route at your own peril, because deleting puppies is pretty sad!

Wrapping Up

Congrats! You made it through every single CRUD action with Express and Mongoose! Now it's time to celebrate 🎉

If you made it this far I just want to say: Thank you and I'm so proud of you!

I encourage you to use what you've learned here to build your own REST API with Express and Mongoose to practice what you've learned.

If you do follow this tutorial and build an API of your own I'd love to see it! Share what you've made with on Twitter

If you want to find out when I put out new blog posts and tutorials, you can follow me on Twitter, where I always post links as soon as my new posts are available.

This tutorial was originally posted on TheCodePixi.dev/blog

💖 💪 🙅 🚩
thecodepixi
Emmy | Pixi

Posted on June 21, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related