Building a Basic Node/Express Server (Part 3)

In part one and part two of this series we explored the basic building blocks of creating a Node/Express server, and added some basic functionality for password encryption, user authentication, etc. Now that those pieces are in place we can work on extending the functionality of our application by adding in the five other user controller methods we’ll need, which are:

  1. Login
  2. Logout
  3. Update
  4. Read (as in, get the user account details from the database)
  5. Delete

Without further adieu, let’s dive into it!

User Login Controller Method

We need to create a new User model method called findByCredentials, which will query the database using the email and password provided by the User on login, and return the user object. In models/user.js:

// src/models/user.js
userSchema.statics.findByCredentials = async (email, password) => {
  const user = await User.findOne({ email });
  if (!user) {
    throw new Error('Unable to login');
  };
  const isMatch = await bcrypt.compare(password, user.password);
  if (!isMatch) {
    throw new Error('Unable to login.');
  };
  return user;
}

With this created, we can use it in the login controller function to find the user who’s attempting to log in, generate an auth token for them if it’s successful, and send the HTTP response:

// src/controllers/user.js
login: async (req, res) => {
  try {
    const user = await User.findByCredentials(
      req.body.email,
      req.body.password
    );
    const token = await user.generateAuthToken();
    res.send({ user, token });
  } catch (e) {
    res.status(400).send(e);
  }
},

Here we’ve written this as an async function with a try/catch block. In the try block we attempt to find the user in question based on their email and password. If this runs with no error, we generate an auth token for the user. Then we send the HTTP response with the user and auth token as an object (no need to set the HTTP status code, as 200 is the default if there’s no error). If we’re unsuccessful in finding the user, we return a 400 status code and send the error back in the HTTP response.

Now we just need to set up a new Postman request to test the login and see if it works:

Another interesting thing to see is if we scroll further down, the tokens array now contains multiple JWT’s:

So we’ve accomplished our goal of setting up the login controller function, and successfully tested some of our previous work as well! With that done we can move on to the next one:

User Logout Controller Method

We should have everything we need in place to get this one working right out of the gate, so in controllers/user.js:

// src/controllers/user.js
logout: async (req, res) => {
  try {
    req.user.tokens = req.user.tokens.filter((token) => token.token !== req.token);
    await req.user.save();
    res.send();
  } catch (e) {
    res.status(400).send();
  }
},

Here we want to go through the array of tokens and remove the one token that matches the one sitting on the user’s current browser, then we save the user object with the token in question removed. Assuming there’s no error, it’ll send back a normal 200 response code indicating it was successful.

We can test this in postman by passing in one of the authorization tokens in the header, and if it’s successful we’ll get back an empty 200 response:

Which we did! So that’s User controller method #2 completed, let’s move on to the next one:

User Read Controller Method

This one will be super easy, since the auth function we created earlier handles all the complicated stuff for us. All we need to do is send the user object back. If they’re not authorized to see the data, then the auth function already handles that:

// src/controllers/user.js
read: async (req, res) => {
  res.send(req.user);
},

Voila. Simplicity at it’s finest. If we run a Postman request to check that it’s working:

We’ll see that the request returns the user object associated with the token we passed in.

User Update Controller Method

Hope you enjoyed the ease of the last one, because the next one might be the most complex of them all! In controllers/user.js:

// src/controllers/user.js
update: async (req, res) => {
  const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
  try {
    if (!user) {
      return res.status(404).send('User with that ID could not be found!');
    };
    await user.save();
    res.send(user);
  } catch (e) {
    res.status(400).send(e);
  };
},

We use the findByIdAndUpdate method to do exactly what it says. We access the id from the params in the URL, and pass in the request.body. In the options object we set new to true so that the request returns the updated user object instead of the old one, and set runValidators to true to re-run the validators to make sure they didn’t try to update their email or password to something invalid.

Then we just check if a valid user object was returned, sending off the appropriate errors if not, save the updated user object, and send it back if it’s successful. If we did it right:

I can add a name to my user account and it will be successfully added, as it was here!

On to the last one for this piece:

User Delete Controller Method

This function is fairly simple as well, since the auth function we created again does most of the heavy lifting. In controllers/user.js:

// src/controllers/user.js
delete: async (req, res) => {
  try {
    await req.user.remove();
    res.send(req.user);
  } catch (e) {
    res.status(400).send(e);
  }
}

Here we simply user the .remove() method called on the user passed in through the request body, and if it’s successful it’ll be removed from the database. If not, it’ll return an error. If we set up a new Postman request and pass in the auth token of an account we want to delete:

We’ll see that it successfully returns the user object in question. And if we check MongoDB Compass to see what our database looks like:

We’ll see that the user in question has indeed been deleted from the database!

And with that, we now have a working Node/Express server with a MongoDB database that can create all the basic CRUD function for User accounts. We still need to set up all of the functionality for the articles (which will be in a later post in this series), but for now we still need to clean up a few details. Mainly, the fact that the HTTP responses are sending the entire user object back including hashed passwords, tokens, etc.

We don’t want that information to be exposed to the user (even if it is for their account, it’s just bad design), so we need to write a function in the User model to remove these fields each time before the HTTP response is sent. In models/user.js:

// src/models/user.js
userSchema.methods.toJSON = function () {
  const user = this;
  const userObject = user.toObject();
  delete userObject.password;
  delete userObject.tokens;
  return userObject;
}

This should be included right after the end of the schema, and right before the generateAuthToken method. If this works as intended and we re-run our User read route in Postman:

We’ll get back a stripped down User object that doesn’t expose any of the data that we don’t want it to.

And there you have it! The user side of our server is mostly completed. We’ll still need to make some changes here and there once we add the Article functionality to this app, as each article needs to be tied to the user that created it, we’ll want to delete all the articles a user created if they delete their account, etc. But for now we’ve put together a pretty solid server!

This exercise has been very useful for me so far, as explaining the code as I wrote it forced me to really understand exactly what I was doing and how I was doing it. If you feel your understanding of the code you write is on a bit of shaky ground, I highly recommend giving an exercise like this a try yourself.

As they say, “the best way to learn is to teach”.

Til the next one!

-Brandon