Build a REST API with Node.js: Finalizing Controllers

Subscribe to my newsletter and never miss my upcoming articles

Hello everyone! Welcome back to Let's Build a Node.js REST API Series. In the previous article, we have integrated our API with MongoDB and set up our Mongoose model. We are now ready to remove the dummy functions in our controller and add actual functions to manipulate our model.

If you are new to this series, please check out the previous articles to follow along:

  1. Designing and Planning the API
  2. Routes and Controllers
  3. Integrating MongoDB Atlas

Important to know: The Request Object

According to Express documentation,

the request object or 'req' represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and so on.

When we make a POST request, we are sending a req.body containing the key-value pairs of data to the server. By default, it is an empty object (i.e. {}).

If we want to create a new tea object and add it to our MongoDB database, we would have to POST our tea object with their keys and values supplied in req.body. We will see how to do this later.

On the other hand, when we make a GET request, we are supplying the value of req.params.{params_name} to ask the server to go retrieve the data that matches that params. By default, it is an empty object (i.e. {}).

Request object.png

For example, in the image above, if the route is /tea/:name, the "name" property is req.params.name, which has a value of 'green'. Hence, we are asking the server to get the tea object with the one that has the name property as 'green'.

Recap

Today's article may be kinda long. After all, we have a total of 6 controller functions to do. A quick refresher of our T-API (Tea API) and its endpoints:

Controller FunctionsRoutesMethodsDescription
newTea/teaPOSTCreates a new tea
getAllTea/teaGETDisplays all tea
deleteAllTea/teaDELETEDeletes all tea
getOneTea/tea/:nameGETDisplays a specific tea
newTeaComment/tea/:namePOSTAdds a comment to a specific tea
deleteOneTea/tea/:nameDELETEDeletes a specific tea

Let's import our tea model we created from the previous article into the controllers/tea.js to get started:

//import tea model
const Tea = require('../models/tea');

Now I shall explain how to write each of the 6 controller functions starting with newTea.

newTea

In this function, we will create a new tea object by supplying its key-value pairs to req.body and then save it to the database. Here's how we can implement it:

  • First, we must make sure we don't accidentally POST a tea with an identical name. So our newTea function should check if the new tea's name from req.body.name has already exists in the database. If it does, don't add this tea.
  • If it doesn't, then create a new tea object with the key-value pairs from the req.body.
  • Save the new tea object to the database.

To check whether a tea name already exists in the database, we can use a mongoose query method called findOne(), which returns one object from the database that matches the condition supplied. More details can be found in their documentation.

//POST tea
const newTea = (req, res) => {
    //check if the tea name already exists in db
    Tea.findOne({name:req.body.name},(data)=>{

        //if tea not in db, add it
        if(data===null){
            //create a new tea object using the Tea model and req.body
            const newTea = new Tea({
                name:req.body.name,
                image: req.body.image, // placeholder for now
                description: req.body.description,
                keywords: req.body.keywords,
                origin: req.body.origin,
                brew_time: req.body.brew_time,
                temperature: req.body.temperature,
            })

            // save this object to database
            newTea.save((err, data)=>{
                if(err) return res.json({Error: err});
                return res.json(data);
            })
        //if tea is in db, return a message to inform it exists            
        }else{
            return res.json({message:"Tea already exists"});
        }
    })    
};

Testing on POSTman

  1. Make sure the method is set to POST and the url is correct.
  2. Click on the 'Body' tab to access the req.body.
  3. Click on the form data radio button below.
  4. Supply some test key-value pairs for the req.body. See example below.

POST with form data.PNG

As you can see, POSTman returns with the data we posted which means our newTea function is working. If you check in MongoDB, you will see that it is indeed in our database.

DB.PNG

getAllTea

To get all tea, our function will retrieve and return all the data from our database using the mongoose built-in find() method. We supply {} as the matching condition so that the all data will be returned.

//GET all teas
const getAllTea = (req, res) => {
    Tea.find({}, (err, data)=>{
        if (err){
            return res.json({Error: err});
        }
        return res.json(data);
    })
};

Testing with POSTman

Make sure we set the method to GET this time and keep the url the same as before. We should get all our tea in our database. Right now, it should return only one tea (black tea) from our newTea POST request before.

getall.PNG

I added another tea object (i.e. green tea) using newTea, and make the getAll request again. Now, I should get 2 tea objects returned.

getall2.PNG

deleteAllTea

This function will delete all data in the database. We can simply do this with deleteMany() and supply the condition parameter with {} since we are deleting everything unconditionally.

//DELETE teas
const deleteAllTea = (req, res) => {
    Tea.deleteMany({}, err => {
        if(err) {
          return res.json({message: "Complete delete failed"});
        }
        return res.json({message: "Complete delete successful"});
    })
};

Testing with POSTman

We set the request method to DELETE and we should see the return message indicating that all data is deleted.

deleteMany.PNG

Now if we try to getAll our tea. We should see an empty array being returned. It works! All data has been deleted.

empty.PNG

getOneTea

This function will retrieve and return only one tea, given its name as the matched condition. We can use findOne() for this. As mentioned earlier about Request Objects, the server will retrieve the tea object with the name from req.params.name.

const getOneTea = (req, res) => {
    let name = req.params.name; //get the tea name

    //find the specific tea with that name
    Tea.findOne({name:name}, (err, data) => {
    if(err || !data) {
        return res.json({message: "Tea doesn't exist."});
    }
    else return res.json(data); //return the tea object if found
    });
};

Testing with POSTman

I re-added back our 2 teas that we've deleted so our database should have green and black tea objects now. We set the url to http://localhost:3000/tea/black%20tea where black%20tea (black tea) is the name of the tea we want to get. We should be returned our black tea object.

GETONE.PNG

If we ask for a tea whose name is not in the database, like "red", we will get the message that it doesn't exist.

notea.PNG

newTeaComment

In this function, the server will POST a comment to a specified tea object's comments property, which is an array. It is implemented as follows:

  • To know which tea to post the comment to, the server will get the tea name from req.params.name, just like getOneTea.
  • Then it takes the comment supplied in req.body.comment to create a comment object and push that comment object to the database, under the specified tea object's comment property.
  • Save the changes
    //POST 1 tea comment
    const newComment = (req, res) => {
      let name = req.params.name; //get the tea to add the comment in
      let newComment = req.body.comment; //get the comment
      //create a comment object to push
      const comment = {
          text: newComment,
          date: new Date()
      }
      //find the tea object
      Tea.findOne({name:name}, (err, data) => {
          if(err || !data || !newComment) {
              return res.json({message: "Tea doesn't exist."});
          }
          else {
              //add comment to comments array of the tea object
              data.comments.push(comment);
              //save changes to db
              data.save(err => {
                  if (err) { 
                  return res.json({message: "Comment failed to add.", error:err});
                  }
                  return res.json(data);
              })  
          } 
      })
    };
    

Testing with POSTman

Just like how we create the test for newTea, we can create a test req.body.comment by supplying a "comment" under POSTman's Body tab. This time, click on the 'raw' radio button and make sure the dropdown is JSON. I added 2 comments and keep the url as http://localhost:3000/tea/black%20 to add comments to the black tea object.

The returned data shows that our black tea object has 2 comments under its 'comments' property. It works!

POSTone.PNG

deleteOneTea

Okay, our last controller function! This function works similar to getOneTea but instead of using findOne we use deleteOne to delete the tea with name that matches req.params.name.

//DELETE 1 tea
const deleteOneTea = (req, res) => {
    let name = req.params.name; // get the name of tea to delete

    Tea.deleteOne({name:name}, (err, data) => {
    if(err || !data) {
        return res.json({message: "Tea doesn't exist."});
    }
    else return res.json({message: "Tea deleted."}); //deleted if found
    });
};

Testing with POSTman

We set the request method to DELETE and have 'black tea' as the name of the tea to be deleted from the database by setting the url to http://localhost:3000/tea/black%20tea (still the same as before).

delOne.PNG

We can check that the deletion works with getAllTea, and see that only green tea is returned because black tea was deleted.

delafter.PNG

Congratulations!

We have built our T-API controller functions! If it pass all the testing with POSTman, we know it works so all there's left to do is to take care of the image property, as it is right now just a dummy string. Uploading an image file for our tea object's image property is a little more complicated than just supplying a string like for 'name'. We will tackle this in the next part and then we are ready to deploy our API!

Thanks for reading and please leave a like or a share if it is helpful. Don't hesitate to ask any questions in the comments below. If there are some concepts you are unsure of, please have a look at some of the reading resources below. Cheers!


Further Reading

Bolaji Ayodeji's photo

The world needs to follow this series. This is just the perfect mini-nodejs course.

Well done Victoria Lo!

Victoria Lo's photo

Hahaha yeah! Thanks Bolaji for the amazing support and comment 🙏

Tapas Adhikary's photo

I am following it and loving it!

Victoria Lo's photo

Thank you! That makes me very happy :)

Rajat Verma's photo

Hey Victoria Lo, Great series till now!!! Helped me to recall the basics. Just a suggestion: it would be much easier to understand if you have used async/await or promise.then/.catch in place of callbacks.

Victoria Lo's photo

Thanks Rajat for enjoying the series and thanks for the feedback!

I see, I thought it would be simpler for beginners if I exclude the concept of Promises and async/await. I'll keep that in mind for future articles :) Thanks again!

Subha Chanda's photo

Another simple but solid explanation. 😃

Victoria Lo's photo

Yay! Always glad you're enjoying this series Subha :)

Richard Harris's photo

Really though, this series gets better and better.. 👏👏

Victoria Lo's photo

Thanks Richard :) That's awesome!

Fernando Torres's photo

Hi! I like so much your series. Thanks for share your knowledge.

I'm trying to code your tutorial on my own repo, but at this point, I can't make that two routes works like this post.

When I'm trying to delete a tea that is not on the collection, the message is the same "Tea deleted". But it dont delete any record, because it doesn't exist at all. Here I'm expecting the message "Tea doesn't exist."

And when post twice a new tea, with all the same values, it insert it anyway when we expect the message "Tea already exists".

The commit is here

I'm going to finish the proyect anyway and expect to solve in the future this issues.