DEV Community

loading...

Part 2: Creating Models for MongoDB with Mongoose

Hal Dunn
a softie and a software engineer
・8 min read

The fully complete codebase for this project is public at THIS github repo, if you would rather just poke around than reading this whole walkthrough.
__

Here's the overview of the two part series. The steps in bold will be covered in this second installment:

  1. Initializing a folder with a package manager
  2. Adding necessary dependencies (and discussing the purposes of each)
  3. Establishing a connection to MongoDB through Atlas
  4. Establishing an Express application & selecting the local port on which to run it
  5. Creating A Model
  6. Creating CRUD routes for that model
  7. Testing your code out with an API tester like Postman or Insomnia

Where we left off in the last post, you should at this point have a successful connection with a MongoDB database hosted on the Atlas website, and a running local server on port 5000. What a time to be alive.

Creating A Model

Organizationally, it is up to you how you arrange your files inside of this app. This sort of backend is not as opinionated as something like Ruby on Rails, so you can exercise a little more freedom with the structure.

To keep my files project organized, I like to keep related items in separate folders, so I'll firstly create a new folder named models, and nest a file called user.model.js inside of that.

This is where we will define the requirements of the data model that we intend to map to our database on MongoDB. Remember that we are using the Mongoose library as the messenger between our Express app and our database, so the first thing to do inside of this file is require Mongoose, and grab the Schema class out of the Mongoose library.

const mongoose = require('mongoose')
const Schema = mongoose.Schema
Enter fullscreen mode Exit fullscreen mode

Now we can begin to write the format for our User model, by creating a new instance of the Schema class.

const userSchema = new Schema()
Enter fullscreen mode Exit fullscreen mode

The first argument for the schema is an object containing the attributes for your data model. Our user model will have 3 attributes: a username, an email, and an age (which is optional). Each attribute will be defined using a key:value pair, with the key being the name of the attribute, and the value being its type. You can check out all available schema types for Mongoose from THIS page. We will just need the String and Number types.

const userSchema = new Schema({
    username: String,
    email: String,
    age: Number
})
Enter fullscreen mode Exit fullscreen mode

This by itself would work, but remember that we wanted the age to be optional. Well, good news, it already is. The default for each model attribute sets required to false. So rather than specify that the age is optional, we should specify that the username and email are not.

const userSchema = new Schema({
    username: { type: String, required: true },
    email: { type: String, required: true },
    age: Number
})
Enter fullscreen mode Exit fullscreen mode

We are allowed to open the value of any attribute out into a full object which specifies further detail for the attribute. For fun let's say you can't use our app unless you're 18 years or older, as well.

const userSchema = new Schema({
    username: { type: String, required: true },
    email: { type: String, required: true },
    age: { type: Number, min: 18 }
})
Enter fullscreen mode Exit fullscreen mode

Validations: easy as that. Any time you make an instance of the user model, it'll run through this Schema to make sure your input attributes meet the requirements.

The last step is to solidify this Schema as a data model with mongoose, and export it from this file for use in other areas of our project.

const User = mongoose.model('User', userSchema)
module.exports = User
Enter fullscreen mode Exit fullscreen mode

Now we can move onto making some CRUD routes for the User.

Creating CRUD routes

In keeping with my organizational choices, I'll now create a folder called controllers which houses the file user.controller.js. This will be the central hub for making any of the activity involving the User model; predictably, controlling what happens when you try to Create, Read, Update, or Delete a model instance.

There are two necessary items to import into this file. Since the User model is going to be needed repeatedly in here, we'll need that one. In addition, we will be using express to define some routes that will live on the local port.

const User = require('../models/user.model')
const router = require('express').Router()
Enter fullscreen mode Exit fullscreen mode

Take a look at that router import -- notice that every time you require the Router, you are calling on a function, which creates a totally new instance of the router within this file. So if we had more than one model, we would have the same code inside of its controller file, but that other router object would be totally different. We are going to define some changes to this instance of the router to make it specific to our User model, then export this instance of the router for the rest of our Express app to use.

With that said, let's lay down the boilerplate for the routes we need, and export it at the bottom.

router.route('/new').post()

router.route('/').get()

router.route('/delete/:id').delete()

router.route('/update/:id').put()

module.exports = router
Enter fullscreen mode Exit fullscreen mode

Now we can get to customizing those routes. Notice that you must first pass the path to the router's route method, then clarify what type of HTTP method will be made to that path. The method, in the first case a post, will accept one argument: a function to run when this path is hit. We will use anonymous functions so that we can keep each route's functionality encapsulated in this one router.route call.

router.route('/new').post((req, res)=>{

})
Enter fullscreen mode Exit fullscreen mode

The function is automatically handed a few arguments: the request (req) information, and the response (res) information. The request comes from your frontend (or Postman/Insomnia), and the response is what your backend should send in response. Each argument has some built-in functionality that we will make use of here. If you have made post requests before, you should be familiar with the format. Your frontend will make a request to your backend and send with it a body attribute which contains the information that is to be posted to the database. The body attribute should look something like this: { username: "Hal", email: "Halrulez@halgoogle.com", age: 247 }. Using that information, we will make a new instance of our user model.

router.route('/new').post((req, res)=>{
    const newUser = new User(req.body)
})
Enter fullscreen mode Exit fullscreen mode

This line will use our Mongoose model to create something that should be acceptable to our MongoDB database. The next step is to actually contact the database and try to save the new instance.

router.route('/new').post((req, res)=>{
    const newUser = new User(req.body)

    newUser.save()
        .then(user => res.json(user))
})
Enter fullscreen mode Exit fullscreen mode

Assuming that the database entry is successful, MongoDB will send us back the newly minted User. There is one key difference between this user instance and the one we created called newUser -- the User that is sent back from MongoDB will have an ID, which we need to use to do all other sorts of operations on this User instance in the future. Once we receive this verified User instance, we use the line res.json(user) to complete the cycle by filling our response's json with the user itself.

The code we wrote should work, but it is a little frail. We did not handle for the case that our new user gets rejected by the database, which could happen for a myriad of reasons. So let's add in some error handling before we move on:

router.route('/new').post((req, res) => {
    const newUser = new User(req.body)

    newUser.save()
        .then(user => res.json(user))
        .catch(err => res.status(400).json("Error! " + err))
})
Enter fullscreen mode Exit fullscreen mode

Now that we have that written out, there's one more step before we can test it. As of right now, the Express app that we created inside server.js doesn't know anything about these model or controller files we've made. So we need to head back over to the server and tell it about our new code.

// inside server.js
const userRoutes = require('./controllers/user.controller')
app.use('/users', userRoutes)
Enter fullscreen mode Exit fullscreen mode

By specifying that the app should use '/users', we are able to nest any routes that are defined in the user controller under the '/users' resource first. So in the case of creating a new user, our frontend should make a post request to 'http://localhost:5000/users/new'.

And now, we can test it!

Testing your code out with an API tester like Postman or Insomnia

I've used both of these apps and enjoy them each equally. No endorsement either way. They do things.
HERE is the link to the Postman tester, and HERE is the link to the Insomnia one.

Right now I happen to be using Insomnia, because the name's cool. Once you've gotten all logged in to your tester, you should create a new request, specify that it is a POST request, copy http://localhost:5000/users/new into the resource section, and select JSON for the body type. Then you can add some raw JSON into the body -- this will match up with what you expect to see from the body portion that your frontend sends. So again something like { username: "Hal", email: "Halrulez@halgoogle.com", age: 247 }. Then send the request! If you've got it all set up properly, you should see a response like this:

Successful Insomnia Post

We got an ID! Massive success.

With the completion of this route, we have a working MEN backend. Of course, we need to complete filling out the other CRUD routes, but the hardest work of ensuring that Express, MongoDB, and Mongoose can chat nicely is over. Now might be another good time for a rousing glass of water.

Since the rest of the routes are simply variations of the first one that we made, I'll put them all in one chunk together that we can look at as a whole:

router.route('/').get((req, res) => {
    // using .find() without a parameter will match on all user instances
    User.find()
        .then(allUsers => res.json(allUsers))
        .catch(err => res.status(400).json('Error! ' + err))
})

router.route('/delete/:id').delete((req, res) => {
    User.deleteOne({ _id: req.params.id })
        .then(success => res.json('Success! User deleted.'))
        .catch(err => res.status(400).json('Error! ' + err))
})

router.route('/update/:id').put((req, res) => {
    User.findByIdAndUpdate(req.params.id, req.body)
        .then(user => res.json('Success! User updated.'))
        .catch(err => res.status(400).json('Error! ' + err))
})
Enter fullscreen mode Exit fullscreen mode

Some items to pay attention to:

  • You can retrieve the ID from the request URL by accessing req.params
  • The update method requires the frontend request to include information for all fields aside from the id -- this is the most straightforward way of updating our database at the present
  • You're in full control of what response is sent back to the frontend. If you wanted to hide the server errors for security reasons, all you would have to do is change what your catch sends back.

And that's it. Congratulations on conquering MEN!

Discussion (1)

Collapse
arcodez profile image
AR

Nice and clear tutorial!