This content originally appeared on DEV Community and was authored by Imran Abdulmalik
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
This will create our package.json file for us:
{
"name": "express-response-helper-demo",
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
}
Create a new folder and name it src
. Inside it, create a new index.js
file. Leave it as it is for now:
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"
}
}
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
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"
}
}
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...');
});
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());
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!' });
});
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
This should spit out the following in your terminal:
With that, our server is up and running. Open a browser tab and enter http:localhost:3000
. You should see something like this:
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;
});
};
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());
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!');
}
});
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...');
});
Just like before, use the following command to start the server:
npm start
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
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;
},
});
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"
},
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));
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
If all goes well, you should see this displayed:
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!');
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:
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.'
}
This is an error object. The helper returns this for every fail
ed 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.');
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 :)
This content originally appeared on DEV Community and was authored by Imran Abdulmalik
Imran Abdulmalik | Sciencx (2021-05-26T15:45:17+00:00) How to handle server responses in ExpressJS – The easy way!. Retrieved from https://www.scien.cx/2021/05/26/how-to-handle-server-responses-in-expressjs-the-easy-way/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.