Build a REST API with Node.js: Finalizing Controllers

Build a REST API with Node.js: Finalizing Controllers

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 need to be able to parse form data with our Express server. We can install the multer package with:
    npm install --save multer
    
    Import multer to our routes/tea.js file:
    const multer = require('multer');
    const upload = multer();
    
    Add upload.none() in the route. This enables our newTea function to read our form data:
    router.post("/tea", upload.none(), teaController.newTea);
    
  • Then, 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.

In controllers/tea.js:

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

        //if tea not in db, add it
        if (!data) {
            //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 there's an error or the tea is in db, return a message         
        }else{
            if(err) return res.json(`Something went wrong, please try again. ${err}`);
            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 there's nothing to delete return a message
    if( data.deletedCount == 0) return res.json({message: "Tea doesn't exist."});
    //else if there's an error, return the err message
    else if (err) return res.json(`Something went wrong, please try again. ${err}`);
    //else, return the success message
    else return res.json({message: "Tea deleted."});
    });
};

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

Did you find this article valuable?

Support Victoria Lo by becoming a sponsor. Any amount is appreciated!

ย