DEV Community

Cover image for How to handle server responses in ExpressJS - The easy way!
Imran Abdulmalik
Imran Abdulmalik

Posted on

How to handle server responses in ExpressJS - The easy way!

Introduction

There are way too many HTTP status codes. If you're like me, you find it difficult to memorize these codes. Fortunately, we don't usually use every HTTP status code out there. There are the common ones that people use often and we can limit our knowledge to just the ones we need.

It would have been great if we had only codes to remember but these codes have meanings (as far HTTP is concerned). So remembering the code only is not enough, we also have to remember what they mean and when to use them. The meanings given to these codes are standard meanings, so if we aim to develop APIs that follows the HTTP standard, it is imperative we use these codes appropriately else we end up creating APIs that others can't understand or use.

Wouldn't it be nice if there was a way we can create API responses without having to worry about which appropriate HTTP code to use? It sure will! Fortunately, there are modules that help us decide which code to use depending on the context. They allow us to be more expressive about what type of response we want our API client to receive (without having to choose the HTTP code ourself, the module will almost always choose the right one for us!).

In this article, we're going to learn an easier way of handling server responses in ExpressJS (and NodeJS). The module we'll be using to achieve this is called express-response-helper.

At the time of writing this article, this module was a relatively new module but it's possible that by the time you read this, the module might have grown because it gets the job done and it's very easy to configure and use.

 

Using Express Response Helper

The documentation for express-response-helper does justice to the module. It covers every aspect of the module with examples. In this article, we'll see how to use the module in a real expressjs application. That said, this article is just to get you started, when it's time to learn more, the documentation is where to head to next.

We're going to create a very simple application to give you ideas on how to use the module on larger projects. You can check out the source codes for the application on Github.

I assume you have NodeJS and NPM installed. If you don't have those, you'll have to install them before proceeding. I used Node v12.21.0 and NPM v7.12.0 but later versions should work just fine. The editor used is Visual Studio Code but of course you can use your favorite IDE.

 

Creating the application

Create a new folder for the application (name it express-response-helper-demo or whatever you prefer) and open the folder with your editor.

Open a terminal and run this command:

npm init -y
Enter fullscreen mode Exit fullscreen mode

This will create our package.json file for us:

{
  "name": "express-response-helper-demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

Create a new folder and name it src. Inside it, create a new index.js file. Leave it as it is for now:

The new folder and file

Modify the package.json to look like this:

{
  "name": "express-response-helper-demo",
  "version": "1.0.0",
  "main": "src/index.js",
  "license": "MIT",
  "scripts": {
    "start": "node src/index.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

We updated the path to the main source file and also added a script command for starting the application.

 

Adding the dependencies

We need the express module and of course the express-response-helper module for this application. Let's add them.

Open a terminal and run this command:

npm install --save express express-response-helper
Enter fullscreen mode Exit fullscreen mode

Once the command finish executing, the package.json file should now look like this:

{
  "name": "express-response-helper-demo",
  "version": "1.0.0",
  "main": "src/index.js",
  "license": "MIT",
  "scripts": {
    "start": "node src/index.js"
  },
  "dependencies": {
    "express": "^4.17.1",
    "express-response-helper": "^1.2.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: depending on when you read this article, these versions will likely change. If some part of this tutorial doesn't work because of version differences please consult the official documentations of the module to learn what changed or you can simply set the versions to match the exact one used in this article.

For this article, the version of express-response-helper used is v1.2.0. Later versions should work too.

With that out of the way, we're all set!

 

Using the module

Open src/index.js and type this:

const express = require('express');
const responseHelper = require('express-response-helper');

const app = express();

// Configure the middleware
app.use(responseHelper.helper());

// Define routes
app.get('/', (req, res) => {
  res.respond({ message: 'Hello, World!' });
});

app.listen(3000, () => {
  console.log('Server running...');
});
Enter fullscreen mode Exit fullscreen mode

What did we just do?

We start by requiring() express that we will use to run the API server. Then we also bring in the express-response-helper module.

require('express-response-helper'); returns an object. The object has two properties: helper() which is a function and responseCodes which is an object with predefined HTTP status codes.

We stored this object inside the responseHelper variable.

Next, we call the express() function and store it inside the app variable. We then register a middleware. Now this is where things get interesting. responseHelper.helper() returns a middleware function that we can attach to our express object. Calling app.use(responseHelper.helper()) registers the middleware for us:

const app = express();

// Configure the middleware
app.use(responseHelper.helper());
Enter fullscreen mode Exit fullscreen mode

It is important we configure the middleware before defining routes. Routes that are defined before registering the middleware won't have access to the functions that the helper adds to our res variable!

 
Next, we define a route:

// Define routes
app.get('/', (req, res) => {
  res.respond({ message: 'Hello, World!' });
});
Enter fullscreen mode Exit fullscreen mode

We define a route for '/'. Inside the route callback function, we send back a response using a respond() function that express-response-helper added for us. Notice how we didn't have to specify the status code for our response. By default, the helper middleware will send 200 which is the correct code to use in this case. The helper will also convert the response body to JSON for us automatically!

Now run the app by running this command:

npm start
Enter fullscreen mode Exit fullscreen mode

This should spit out the following in your terminal:

Terminal Output

With that, our server is up and running. Open a browser tab and enter http:localhost:3000. You should see something like this:

Server Response

As you can see, the helper middleware is working as expected. We've only just scratched the surface. Let's look at a more complex example with more routes.

 

Extending the API

Let's build a more practical example. For simplicity, we won't be using any real database. Our aim is to see how the helper middleware works for different response types, it doesn't matter where data come from.

Open src/index.js and these helper variables and fucntions before the route definition:

// Create a database for users
const database = [
  {
    username: 'user1',
    email: 'user1@fake.com',
    password: 'test1',
  }
];

// A function for validating email addresses
const validEmail = email => {
  const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
  return re.test(String(email).toLowerCase());
};

// A function to check if a username is unique
const isUsernameUnique = username => {
  let isUnique = true;

  database.forEach(user => {
    if (user.username === username)
      isUnique = false;
  });

  return isUnique;
};

// A function to check if an email is unique
const isEmailUnique = email => {
  let isUnique = true;

  database.forEach(user => {
    if (user.email === email.toLowerCase())
      isUnique = false;
  });

  return isUnique;
};

// A function that returns a the index of a user data given the username
const findUser = username => {
  return database.findIndex(user => {
    return user.username === username;
  });
};
Enter fullscreen mode Exit fullscreen mode

Next, let's add a built-in express middleware that will help us parse data passed to our API. Add this just below where we configured the helper middleware:

app.use(express.json());
Enter fullscreen mode Exit fullscreen mode

Finally, add these new route definitions to complete our API (remove the previous route):

// Define routes
app.get('/', (req, res) => {
  res.respondNoContent();
});

// To add a user
app.post('/user', (req, res) => {
  const body = req.body;
  if (body.username && body.email && body.password) {
    // Make sure the username and email is unique

    if (!isUsernameUnique(body.username)) {
      // Duplicate username
      res.failValidationError('username is taken.');
      return;
    }

    if (!isEmailUnique(body.email)) {
      // Duplicate email
      res.failValidationError('email is taken.');
      return;
    }

    // Insert the user
    const user = {
      username: body.username,
      email: body.email.toLowerCase(),
      password: body.password,
    };

    // Add to the database
    database.push(user);

    // Return a response confirming creation
    res.respondCreated('User Account Created!');
  }
  else {
    // If some or all the required data is not provided, return a failed response
    res.failValidationError('Please provide all required data!');
  }
});

// To update a user
app.put('/user/:username', (req, res) => {
  // Find the user
  const index = findUser(req.params.username);
  const body = req.body;

  if (index !== -1) {
    if (body.email) {
      // Get the user
      const user = database[index];

      // If the email equals the current one, do nothing
      if (body.email === user.email) {
        // Return a response confirming update
        res.respondUpdated('User account updated.');
      }
      else {
        // Make sure the email is unqiue
        if (!isEmailUnique(body.email)) {
          // Duplicate email
          res.failValidationError('email is taken.');
          return;
        }

        // Update the email
        user.email = body.email;

        // Return a response confirming update
        res.respondUpdated('User account updated.');
      }
    }
    else {
      // Return a failed response
      res.failValidationError('Please provide all required data!');
    }
  }
  else {
    // User not found.
    res.failNotFound('No user with such username exists!');
  }
});

// To remove a user
app.delete('/user/:username', (req, res) => {
  // Find the user
  const index = findUser(req.params.username);

  if (index !== -1) {
    // Remove the user
    database.splice(index);

    // Return a response confirming removal
    res.respondDeleted('User removed!');
  }
  else {
    // User not found.
    res.failNotFound('No user with such username exists!');
  }
});

// To authenticate a user
app.post('/login', (req, res) => {
  const body = req.body;
  if (body.username && body.password) {
    // Make sure the username and email is unique

    // Find the user
    const index = findUser(body.username);

    if (index !== -1) {
      // Get the user 
      const user = database[index];

      // Authenticate
      if (user.password === body.password) {
        // Authenticated, return basic user data
        res.respond({ username: user.username, email: user.email });
      }
      else {
        // return a response indicating that access is denied
        res.failUnathorized('Invalid password!');
      }
    }
    else {
      // User not found.
      res.failNotFound('No user with such username exists!');
    }
  }
  else {
    // If some or all the required data is not provided, return a failed response
    res.failValidationError('Please provide all required data!');
  }
});
Enter fullscreen mode Exit fullscreen mode

We've defined routes to perform some basic CRUD operations. After those additions, your src/index.js should now look like this:

const express = require('express');
const responseHelper = require('express-response-helper');

const app = express();

// Create a database for users
const database = [
  {
    username: 'user1',
    email: 'user1@fake.com',
    password: 'test1',
  }
];

// A function for validating email addresses
const validEmail = email => {
  const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
  return re.test(String(email).toLowerCase());
};

// A function to check if a username is unique
const isUsernameUnique = username => {
  let isUnique = true;

  database.forEach(user => {
    if (user.username === username)
      isUnique = false;
  });

  return isUnique;
};

// A function to check if an email is unique
const isEmailUnique = email => {
  let isUnique = true;

  database.forEach(user => {
    if (user.email === email.toLowerCase())
      isUnique = false;
  });

  return isUnique;
};

// A function that returns a the index of a user data given the username
const findUser = username => {
  return database.findIndex(user => {
    return user.username === username;
  });
};

// Configure the middlewares
app.use(responseHelper.helper());
app.use(express.json());

// Define routes
app.get('/', (req, res) => {
  res.respondNoContent();
});

// To add a user
app.post('/user', (req, res) => {
  const body = req.body;
  if (body.username && body.email && body.password) {
    // Make sure the username and email is unique

    if (!isUsernameUnique(body.username)) {
      // Duplicate username
      res.failValidationError('username is taken.');
      return;
    }

    if (!isEmailUnique(body.email)) {
      // Duplicate email
      res.failValidationError('email is taken.');
      return;
    }

    // Insert the user
    const user = {
      username: body.username,
      email: body.email.toLowerCase(),
      password: body.password,
    };

    // Add to the database
    database.push(user);

    // Return a response confirming creation
    res.respondCreated('User Account Created!');
  }
  else {
    // If some or all the required data is not provided, return a failed response
    res.failValidationError('Please provide all required data!');
  }
});

// To update a user
app.put('/user/:username', (req, res) => {
  // Find the user
  const index = findUser(req.params.username);
  const body = req.body;

  if (index !== -1) {
    if (body.email) {
      // Get the user
      const user = database[index];

      // If the email equals the current one, do nothing
      if (body.email === user.email) {
        // Return a response confirming update
        res.respondUpdated('User account updated.');
      }
      else {
        // Make sure the email is unqiue
        if (!isEmailUnique(body.email)) {
          // Duplicate email
          res.failValidationError('email is taken.');
          return;
        }

        // Update the email
        user.email = body.email;

        // Return a response confirming update
        res.respondUpdated('User account updated.');
      }
    }
    else {
      // Return a failed response
      res.failValidationError('Please provide all required data!');
    }
  }
  else {
    // User not found.
    res.failNotFound('No user with such username exists!');
  }
});

// To remove a user
app.delete('/user/:username', (req, res) => {
  // Find the user
  const index = findUser(req.params.username);

  if (index !== -1) {
    // Remove the user
    database.splice(index);

    // Return a response confirming removal
    res.respondDeleted('User removed!');
  }
  else {
    // User not found.
    res.failNotFound('No user with such username exists!');
  }
});

// To authenticate a user
app.post('/login', (req, res) => {
  const body = req.body;
  if (body.username && body.password) {
    // Make sure the username and email is unique

    // Find the user
    const index = findUser(body.username);

    if (index !== -1) {
      // Get the user 
      const user = database[index];

      // Authenticate
      if (user.password === body.password) {
        // Authenticated, return basic user data
        res.respond({ username: user.username, email: user.email });
      }
      else {
        // return a response indicating that access is denied
        res.failUnathorized('Invalid password!');
      }
    }
    else {
      // User not found.
      res.failNotFound('No user with such username exists!');
    }
  }
  else {
    // If some or all the required data is not provided, return a failed response
    res.failValidationError('Please provide all required data!');
  }
});

app.listen(3000, () => {
  console.log('Server running...');
});
Enter fullscreen mode Exit fullscreen mode

Just like before, use the following command to start the server:

npm start
Enter fullscreen mode Exit fullscreen mode

The server should start running. Leave it that way (do not kill the terminal), we're going to interact with it next.

The browser can only send GET requests for us, we need to be able to send other types of requests like POST, PUT, DELETE. For this, we'll create a separate client code to consume our API. We could use tools like curl but let's take the testing off the command line to see how a real client can consume our client.

First, let's add axios. We'll use it to send requests to our server. Open a new terminal and run this command:

npm install --save axios
Enter fullscreen mode Exit fullscreen mode

Now create a new file client.js inside the src folder. Add this to the file:

const axiosModule = require('axios');

const base_url = 'http://localhost:3000/';

const axios = axiosModule.default.create({
  baseURL: base_url,
  validateStatus: (status) => {
    return status >= 200 && status < 500;
  },
});
Enter fullscreen mode Exit fullscreen mode

This configures axios. We set base_url to the location of our API. We also tell axios to allow us handle HTTP status codes between 200 and 500 ourselves.

Finally, modify the "scripts" property in our package.json file:

"scripts": {
  "start": "node src/index.js",
  "client": "node src/client.js"
},
Enter fullscreen mode Exit fullscreen mode

We added a command (client) that we can use to run the client code. Now we can start sending requests!

Open src/client.js and add this code below the current contents:

// Create a user (with valid data)
axios.post('user', {
  username: 'user2',
  email: 'user@fake.com',
  password: 'test2',
})
.then(res => {
  console.log({
    code: res.status,
    response: res.data,
  })
})
.catch((error) => console.log(error));
Enter fullscreen mode Exit fullscreen mode

This will send a POST request to the /user endpoint. When we get a response, we simply log both the HTTP status code and the data that we receive.

Make sure the terminal for the express server is still running. Now open a new terminal and run this command:

npm run client
Enter fullscreen mode Exit fullscreen mode

If all goes well, you should see this displayed:

Alt Text

Great! Our API workse fine. Now if you check back the source code for the route .post(/user) you'll see that we didn't have to know what status code to send, we just know that we want our response to confirm that a user was created. That's the power of express-response-helper!

To refresh your memory, here's the bit of code sending the response:

res.respondCreated('User Account Created!');
Enter fullscreen mode Exit fullscreen mode

Because our API was programmed to prevent duplicates, it won't let us add the same user twice. Make sure the terminal for the server is still running, now run the command again: npm run client.

You should get this output:

Alt Text

The output is different because we attempted to add an existing username. Notice the type of response returned by express-response-helper:

{ 
  status: 400, 
  error: 400, 
  messages: 'username is taken.' 
}
Enter fullscreen mode Exit fullscreen mode

This is an error object. The helper returns this for every failed requests. It clear tell us the status of the error and a description (that we provided, though the helper has sensible defaults for error description).

To refresh your memory again let's look at the bit of code producing this result:

res.failValidationError('username is taken.');
Enter fullscreen mode Exit fullscreen mode

We just gave the helper a secription of the error message and it threw a detailed error object back to the client. Again, we didn't have to decide the HTTP status code!

This article is about server responses, not API consumption. So I'll stop here. As an exercise, go ahead and test the remaining endpoints. I've commented the source code to help you understand the code quicky.

As you read through the source code, notice how less often you have to worry about the HTTP status codes needed. express-response-helper allow us write expressive code for responses and this makes it easier for us (and others) to quicky understand what our code snippet is doing.

 

Goodbye!

We've come to the end of this article. I hope you learnt something new. This is probably a good time to head to the documentation for express-response-helper to learn more.

As a final note, I am a contributor to this module, so if you tried it and you didn't like it, send me a mail :)

Discussion (0)