Understanding HTTP response status codes in frontend applications - Part 2: Implementations
You will need Node 8+ and Postman installed on your machine.
In this tutorial, we will do three things to illustrate how these status codes work:
- Build a simple Node.js backend using Express to send data.
- Write simple HTML pages and make API calls using JavaScripts fetch API.
- Use Postman for some of our tests
If you are not familiar with Node.js, do not worry. We are not building anything complex.
In the last tutorial of this series, we did an extensive deep dive into HTTP status codes and what they mean. If you are a backend developer, it is likely to alter how you build applications that other people will interact with. If you are a frontend developer, you will now understand how best to use the data sent to you from an API or even the backend of the application you are building (say you are not using APIs).
Prerequisites
- You have read the first part of this tutorial
- You understand HTML and JavaScript
The Node.js backend
We are going to specify simple one liners on our Node backend so we can keep this guide simple.
First, we are going to create a new folder for our project called http-response-codes
and navigate into the created folder. We then proceed to create a new Node project:
$ npm init -y
This will create a new project with the default settings which is fine for this project
Install dependencies
$ npm install --save express body-parser
Create the server
We have all we need to begin. Create a file index.js
in the root directory of the project. This is where we will specify our Node server.
Open the file and add the following:
// index.js
/* A simple API skeleton in Node.js using express */
const express = require('express')
const bodyParser = require('body-parser')
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
We have imported the dependencies we need for the project. Now, let’s define some routes.
Add the routes for success status codes:
// index.js
/** The success status codes */
// 200 Ok
app.get('/', (req, res) => res.status(200).sendFile(`${__dirname}/html/register.html`));
app.get('/update', (req, res) => res.status(200).sendFile(`${__dirname}/html/update.html`));
app.get('/bad-request', (req, res) => res.status(200).sendFile(`${__dirname}/html/bad-request.html`));
app.get('/complete', (req, res) => res.status(200).sendFile(`${__dirname}/html/complete.html`));
app.get('/old-registration', (req, res) => res.status(200).sendFile(`${__dirname}/html/old-register.html`));
app.get('/old-reg-fail', (req, res) => res.status(200).sendFile(`${__dirname}/html/old-reg-fail.html`));
app.get('/user/john-new.html', (req, res) => res.status(200).send({message : "This is John's new page"}));
app.get('/user/jane-new', (req, res) => res.status(200).send({message : "This is Jane's new page"}));
app.get('/thank-you-page', (req, res) => res.status(200).send({message : "Thank you for registering!"}));
// 201 Created
app.post('/register', (req, res) => {
// logic to save to database
res.status(201).send({message : 'registrations compelete'})
});
app.post('/login', (req, res) => {
// logic to login
res.status(201).send('You have been logged in')
});
// 204 No Content
app.put('/update', (req, res) => {
// logic to update database record
res.status(204).end()
});
We have defined a few routes to allow us test out our endpoints. We will test the success status codes
with our browser so we can see the entire process the connection takes when we make it. Let’s worry about this later.
Add the routes for redirection status codes:
// index.js
/** The redirection status codes */
// 301 Moved Permanently
app.get('/user/john', (req, res) => {
res.set('location', '/user/john-new.html')
res.status(301).send()
});
// This redirection may fail because the browser might change the request method to GET
app.post('/old-registration-fail', (req, res) => {
res.set('location', '/register')
res.status(301).send()
});
// 302 Found
app.get('/user/jane', (req, res) => {
res.set('location', '/user/jane-new')
res.status(302).send()
});
// 303 See Other
app.post('/complete-registration', (req, res) => {
res.set('location', '/thank-you-page')
res.status(303).send()
});
// 307 Temporal Redirect
app.post('/old-registration', (req, res) => {
res.set('location', '/register')
res.status(307).send()
});
// 308 Permanent Redirect
app.post('/old-login', (req, res) => {
res.set('location', '/login')
res.status(308).send()
});
Add the routes for client error status codes:
// index.js
/** Client error status codes */
// 400 Bad Request
app.post('/bad-request', (req, res) => {
res.status(400).send({message : "You are missing vital credentials"})
});
// 401 Unauthorized
app.get('/user', (req, res) => {
res.status(401).send({message : "You need to login to view this"})
});
// 403 Forbidden
app.get('/super-secret', (req, res) => {
res.status(403).send({message : "You are forbidden from seeing this"})
});
// 405 Method Not Allowed
app.all('/only-put', (req, res) => {
if(req.method == "PUT") res.status(204).end()
else res.status(405).send({message : "Please use put"})
})
Add the routes for server error status codes:
// index.js
/** Server error status codes */
// 500 Internal Server Error
app.post('/500', (req, res) => {
res.status(500).send({message : "I failed. I'm sorry"})
});
// 501 Unauthorized
app.patch('*', (req, res) => {
res.status(501).send({message : "I will patch no such thing"})
});
// 503 Service Unavailable
app.get('/503', (req, res) => {
res.status(503).send({message : "I had to take a break. Getting too old for this"})
});
// 505 Method Not Allowed
app.all('/http2', (req, res) => {
if(req.httpVersion == "2.0") res.status(200).send({message : "You get a response. She gets a response. They get a response... Everybody gets a response"})
else res.status(505).send({message : "Only http2 baby"})
})
We put it last because we want to catch everything else not defined. We will also start the server.
// index.js
// 404 Not Found
app.all('*', (req, res) => {
res.status(404).send({message : "This resource was not found"})
})
app.listen(3000, () => console.info('Application running on port 3000'));
Do not get overwhelmed by the many lines of code above. They are all routes that return a message when you hit them. We do not have any complex implementation so that we can keep this guide simple for everyone to follow.
The HTML and JavaScript frontend
For this section, we are going to create a directory — html
in the root directory of our project. Inside that directory, we will make all the HTML files we referenced in our code above.
Create the directory:
$ mkdir html
Create the register file:
<!-- html/register.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Register Page</title>
</head>
<body>
<h2>Please ignore the lack of styling</h2>
<form id="form">
<input type="text" name="name">
<input type="email" name="email">
<input type="password" name="password">
<input type="submit" name="submit">
</form>
<script type="text/javascript">
// handle form submission
</script>
</body>
</html>
Let’s add the script to handle form submission:
// html/register.html
[...]
let form = document.getElementById('form')
let data = {
name : form.elements.name.value,
email : form.elements.email.value,
password : form.elements.password.value
}
form.addEventListener('submit', (e) => {
e.preventDefault()
fetch('http://localhost:3000/register', {
method: 'POST',
body: JSON.stringify(data),
headers:{
'Content-Type': 'application/json'
}
}).then(res => {
console.log(res) // log response object
return res.json() // return json data from the server
})
.then(response => alert(response.message))
.catch(error => console.error('Error:', error))
})
[...]
We are using JavaScript’s Fetch API to submit the form to our server. Then we are logging the response object we got, before returning the data sent from the server in json
format. The reason for logging the response is so we can look at the console to view what happened behind the scenes. You will notice that for any redirect response sent from the server, fetch
will redirect automatically, and send us a result. If you do not look inside, you may never know.
We chain another then
whenever we return json
data as fetch will resolve the data into a promise, which is handled by the next then
we chained. You will see it at work shortly.
For now, let’s make the remaining pages which follow a similar pattern.
Create the update file:
<!-- html/update.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Register Page</title>
</head>
<body>
<h2>Please ignore the lack of styling</h2>
<form id = "form">
<input type="text" name="name" value="AceKyd">
<input type="email" name="email" value="ace@kyd.com">
<input type="password" name="password" value="acekyd">
<input type="submit" name="submit">
</form>
<script type="text/javascript">
let form = document.getElementById('form')
let data = {
name : form.elements.name.value,
email : form.elements.email.value,
password : form.elements.password.value
}
form.addEventListener('submit', (e) => {
e.preventDefault()
fetch('http://localhost:3000/update', {
method: 'PUT',
body: JSON.stringify(data),
headers:{
'Content-Type': 'application/json'
}
}).then(response => {
if(response.status == 204) alert("Data updated")
})
.catch(error => console.error('Error:', error))
})
</script>
</body>
</html>
Create the old-register file:
<!-- html/old-register.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Register Page</title>
</head>
<body>
<h2>Please ignore the lack of styling</h2>
<form id="form">
<input type="text" name="name">
<input type="email" name="email">
<input type="password" name="password">
<input type="submit" name="submit">
</form>
<script type="text/javascript">
let form = document.getElementById('form')
let data = {
name : form.elements.name.value,
email : form.elements.email.value,
password : form.elements.password.value
}
form.addEventListener('submit', (e) => {
e.preventDefault()
fetch('http://localhost:3000/old-registration', {
method: 'POST',
body: JSON.stringify(data),
headers:{
'Content-Type': 'application/json'
}
}).then(res => {
console.log(res)
return res.json()
})
.then(response => alert(response.message))
.catch(error => console.error('Error:', error))
})
</script>
</body>
</html>
Create the old-reg-fail file:
<!-- html/old-reg-fail.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Register Page</title>
</head>
<body>
<h2>Please ignore the lack of styling</h2>
<form id="form">
<input type="text" name="name">
<input type="email" name="email">
<input type="password" name="password">
<input type="submit" name="submit">
</form>
<script type="text/javascript">
let form = document.getElementById('form')
let data = {
name : form.elements.name.value,
email : form.elements.email.value,
password : form.elements.password.value
}
form.addEventListener('submit', (e) => {
e.preventDefault()
fetch('http://localhost:3000/old-registration-fail', {
method: 'POST',
body: JSON.stringify(data),
headers:{
'Content-Type': 'application/json'
}
}).then(res => {
console.log(res)
return res.json()
})
.then(response => alert(response.message))
.catch(error => console.error('Error:', error))
})
</script>
</body>
</html>
Create the old-login file:
<!-- html/old-login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Register Page</title>
</head>
<body>
<h2>Please ignore the lack of styling</h2>
<form id="form">
<input type="email" name="email">
<input type="password" name="password">
<input type="submit" name="submit">
</form>
<script type="text/javascript">
let form = document.getElementById('form')
let data = {
name : form.elements.name.value,
password : form.elements.password.value
}
form.addEventListener('submit', (e) => {
e.preventDefault()
fetch('http://localhost:3000/old-login', {
method: 'POST',
body: JSON.stringify(data),
headers:{
'Content-Type': 'application/json'
}
}).then(res => {
console.log(res)
return res.json()
})
.then(response => alert(response.message))
.catch(error => console.error('Error:', error))
})
</script>
</body>
</html>
Create the bad-request file:
<!-- html/bad-request.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Register Page</title>
</head>
<body>
<h2>Please ignore the lack of styling</h2>
<form id = "form">
<input type="text" name="name" value="AceKyd">
<input type="email" name="email" value="ace@kyd.com">
<input type="password" name="password" value="acekyd">
<input type="submit" name="submit">
</form>
<script type="text/javascript">
let form = document.getElementById('form')
let data = {
name : form.elements.name.value,
email : form.elements.email.value,
password : form.elements.password.value
}
form.addEventListener('submit', (e) => {
e.preventDefault()
fetch('http://localhost:3000/bad-request', {
method: 'POST',
body: JSON.stringify(data),
headers:{
'Content-Type': 'application/json'
}
}).then(res => {
console.log(res)
return res.json()
}).then(res => alert(res.message))
.catch(error => console.error('Error:', error))
})
</script>
</body>
</html>
Create the complete file:
<!-- html/complete.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Register Page</title>
</head>
<body>
<h2>Please ignore the lack of styling</h2>
<form id = "form">
<input type="text" name="name" value="AceKyd">
<input type="email" name="email" value="ace@kyd.com">
<input type="password" name="password" value="acekyd">
<input type="submit" name="submit">
</form>
<script type="text/javascript">
let form = document.getElementById('form')
let data = {
name : form.elements.name.value,
email : form.elements.email.value,
password : form.elements.password.value
}
form.addEventListener('submit', (e) => {
e.preventDefault()
fetch('http://localhost:3000/complete-registration', {
method: 'POST',
body: JSON.stringify(data),
headers:{
'Content-Type': 'application/json'
}
}).then(res => {
console.log(res)
return res.json()
}).then(res => alert(res.message))
.catch(error => console.error('Error:', error))
})
</script>
</body>
</html>
Now, we are ready to test 😁.
Run our application:
$ node index
Testing the endpoints on the browser starting with the 2xx endpoints
We are going to see a series of tests done on this endpoint and see how they all work together. The screenshots below would show two browser windows side by side to show the different stages of the request, responses and additional details as necessary. Shall we?
The homepage
Visit localhost:3000/
in the browser to access the homepage, which matches our app.get('/')
route as declared.
Notice the status code? That is the expected result when a get request is completed successfully. Be it an API call or direct browser request. When you check the result of every browser page we load, you will see that it the Status Code
will be 200 OK
.
Submitting the registration form
Here we enter data into the form and click on the Submit button to make a POST request to our route as defined app.post('/register')
Look at the data returned from each request. Now, we can proceed to tell our users they have been successfully registered and their accounted created.
Making an update
Here we visit localhost:3000/update
matching the app.put('/update')
route, which loads up a pre-filled form to simulate editing an existing resource on the server.
As is expected from a PUT
request, there should be no response data once we send a 204
. With such a request, do not expect anything new to be created on the server, as it will return the resource you gave to it.
A good question is “What if we are changing a profile image? Wouldn’t we need the server to send us the URL it saved the image with so we can load the image?” Since you are creating a resource, you should send such a request over POST
, even though it will update a record already existing on your system.
Now to 3xx endpoints
Send a request to an old registration link
Visit localhost:3000/old-registration
matching app.get('/old-registration')
to send a request to app.post('/old-registration')
.
You can learn now that most of ajax
clients like fetch
will perform redirection automatically. Good thing our browser records activities, if not, we may not have seen how this worked. Notice also that they tell you if they were redirected or not.
Whatever it means for you at this point may not be clear to me. But if you keep records of changes for future requests (say your application learns and autoupdates it’s information), you can now note this change.
Send a request to the old registration link bound to fail
Visit localhost:3000/old-registration
matching app.get('/old-reg-fail')
.
You see that we used 301
for this redirection and the browser changed the request method from POST
to GET
. Of course, our register
endpoint does not have a GET
method.
Let’s get Jane’s profile
Visit localhost:3000/user/jane
matching app.get('/user/jane')
.
The server found the profile quite alright, but we have to go somewhere else to see it. There could be so many reasons for this. I can have a public link for viewing profiles for a class of users on my application. The endpoint might have very limited data based on the user level.
Be that as it may, you should know how many legs your application is taking. Also, notice the URL changed in the URL bar 😃.
Try and get John’s profile.
Let’s complete registration
Visit localhost:3000/complete
matching app.get('/complete')
We were taken to a completely new page. Now, it is up to you to change the URL of your browser as you display the content. Can you see how this will help you create richer experience for your applications?
Now to 4xx endpoints
Making a bad request
Visit localhost:3000/bad-request
matching app.get('/bad-request')
.
My favorite status code 😃. This tells you, me, the user that we supplied wrong credentials and the request will not go through. You can send a nicer message to your users to tell them they sent wrong credentials.
For the rest of the tests we will do, we will use Postman.
Let’s access all user details we should only see if we are logged in
Here, we will be making a GET
request to http://localhost:3000/user
to match app.get('/user')
. We will also need to add a Content-Type
header set to application/json
to specify what the content type of the returned content actually is. And in this case, JSON is returned.
Look at the Status
and then the Body
of the response.
Let’s access a super secret page
Here, we will be making a GET
request to http://localhost:3000/super-secret
to match app.get('/super-secret')
.
Let’s access the endpoint that accepts only PUT requests
Here, we will be making requests to http://localhost:3000/only-put
with multiple request methods.
We already tried GET
and POST
and we still get 405 Method Not Allowed
. Let’s try PUT
And we get 204
which means our request was successful.
Let’s try an arbitrary endpoint and see if our 404 works
Here, we will be making a random GET
request to http://localhost:3000/hello-mike
that will be matched by our generic route – app.all('*')
.
Yep. There you have it.
Finally to 5xx endpoints
The 500 error
Here, we will be making a POST
request to http://localhost:3000/500
to match app.post('/500')
.
Try patch method that we did not implement
Here, we will be making a random PATCH
request to http://localhost:3000/501
that will be matched by our generic route – app.patch('*')
.
How you handle a 501
will depend on what the developer making the API
Let’s make a request when the server is not available
Here, we will be making a GET
request to http://localhost:3000/503
to match app.get('/503')
.
The unsupported HTTP version
Here, we will be making a GET
request to http://localhost:3000/http2
that will be matched by our route – app.all('http2')
.
And finally, we have come to the end of tests… Phew!
Conclusion
This has been an interesting journey learning about status codes and what each mean. We tested very common endpoints to see what the response on them will look like.
Now, you can build better applications both on the backend and frontend and create more engaging experiences for your users.
Checkout the code to the project on GitHub.
27 November 2018
by Abati Adewale