© Francesco Strazzullo 2019
F. StrazzulloFrameworkless Front-End Developmenthttps://doi.org/10.1007/978-1-4842-4967-3_5

5. HTTP Requests

Francesco Strazzullo1 
(1)
TREVISO, Treviso, Italy
 

In the previous chapters, you learned to render DOM elements and to react to events from the system or users, but a front-end application feeds on asynchronous data from a server. The purpose of this chapter is to show you how to build an HTTP client in a frameworkless way.

A Bit of History: The Birth of AJAX

Before 1999, a complete page reload was required for every user action needing any kind of data from the server. For people that are approaching web development (or the web in general) today, it’s very hard to imagine web applications built in this way. In 1999, applications like Outlook, Gmail, and Google Maps started using a new technique: loading data from a server after the initial page load, without completely reloading the page. Jesse James Garrett, in his 2005 blog post ( https://adaptivepath.org/ideas/ajax-new-approach-web-applications/ ), named this technique AJAX, an acronym for Asynchronous JavaScript and XML.

The main part of any AJAX applications is the XMLHttpRequest object. As you will see later in this chapter, with this object, you can fetch data from a server with an HTTP request. The W3C made a first draft of the specification for this object in 2006.

As I mentioned, the “X” in AJAX stands for XML. When AJAX came out, the web applications received from the server data in XML form. Today, however, the friendlier JSON (for JavaScript applications) format is used. In Figure 5-1 you can see the differences between AJAX and Non-AJAX architectures.
../images/476371_1_En_5_Chapter/476371_1_En_5_Fig1_HTML.png
Figure 5-1

AJAX vs. Non-AJAX architecture

A todo-list REST Server

To test the clients that we are going to develop, we need a server to fetch data from. Listing 5-1 shows a very simple REST server for Node.js with Express ( https://expressjs.com ), a simple library to quickly create REST servers . This dummy server will use a temporary array to store the data related to our todo-list, instead of a real database. To generate fake IDs, I used a small npm package called UUID ( www.npmjs.com/package/uuid ) that lets developers generate universally unique identifiers (UUIDs).
const express = require('express')
const bodyParser = require('body-parser')
const uuidv4 = require('uuid/v4')
const findIndex = require('lodash.findindex')
const PORT = 8080
const app = express()
let todos = []
app.use(bodyParser.json())
app.get('/api/todos', (req, res) => {
  res.send(todos)
})
app.post('/api/todos', (req, res) => {
  const newTodo = {
    completed: false,
    ...req.body,
    id: uuidv4()
  }
  todos.push(newTodo)
  res.status(201)
  res.send(newTodo)
})
app.patch('/api/todos/:id', (req, res) => {
  const updateIndex = findIndex(
    todos,
    t => t.id === req.params.id
  )
  const oldTodo = todos[updateIndex]
  const newTodo = {
    ...oldTodo,
    ...req.body
  }
  todos[updateIndex] = newTodo
  res.send(newTodo)
})
app.delete('/api/todos/:id', (req, res) => {
  todos = todos.filter(
    t => t.id !== req.params.id
  )
  res.status(204)
  res.send()
})
app.listen(PORT)
Listing 5-1

A Dummy REST Server for Node.js

Representational State Transfer

In this section, I explain the meaning of REST, which is the architecture behind our dummy server. If you already know about REST, you can simply skip this section.

REST is an acronym for REpresentational State Transfer, which is a way to design and develop web services. The main abstraction of any REST API is its resources. You need to split your domain into resources. Each resource should be read or manipulated and accessed at a specific URI (Uniform Resource Identifier). For example, to see a list of the users in your domain, you should use this URI: https://api.example.com/users/ . To read the data for a specific user, the URI should have this form: https://api.example.com/users/id1 (where id1 is the ID of the user that you want to read).

To manipulate the users (add, remove, or update), the same URIs are used, but with different HTTP verbs. Table 5-1 contains some examples of REST APIs for manipulating a list of users.
Table 5-1

REST API Cheat Sheet

Action

URI

HTTP Verb

Read all users’ data

https://api.example.com/users/

GET

Read data of user with ID “1”

https://api.example.com/users/1

GET

Create a new user

https://api.example.com/users/

POST

Replace user data with ID “1”

https://api.example.com/users/1

PUT

Update user data with ID “1”

https://api.example.com/users/1

PATCH

Delete the user with ID “1”

https://api.example.com/users/1

DELETE

The actions listed in this table are straightforward. The only topic that may need an explanation the difference between updating the data (with PATCH) and replacing the data (with PUT). When you use the verb PUT, you need to pass in the body of the HTTP requests the new user, complete in all its parts. When PATCH is used, the body should contain only the differences with the previous state. In this scenario, the new todo object is the result of the merging of the old todo with the request body.

We have only scratched the surface of REST APIs. If you want to go deeper, I suggest reading RESTful Web APIs: Services for a Changing World by Leonard Richardson and Mike Amundsen (O’Reilly Media, 2013).

Code Examples

We are going to create three different HTTP clients with three different technologies: XMLHttpRequest, Fetch, and axios. We will analyze each client’s strengths and weaknesses.

The Basic Structure

To show how our HTTP clients work, we will always use the same simple application that is shown in Figure 5-2. To keep the focus on the HTTP client, we are not going to use a TodoMVC application, but a simpler application with buttons that execute the HTTP requests and print the result on the screen. The code for this application (and the other implementations) is at https://github.com/Apress/frameworkless-front-end-development/tree/master/Chapter05/ .
../images/476371_1_En_5_Chapter/476371_1_En_5_Fig2_HTML.jpg
Figure 5-2

The application used to test the HTTP clients

In Listing 5-2, you can see our application’s index.html. Listing 5-3 shows the main controller.
<html>
<body>
    <button data-list>Read Todos list</button>
    <button data-add>Add Todo</button>
    <button data-update>Update todo</button>
    <button data-delete>Delete Todo</button>
    <div></div>
</body>
</html>
Listing 5-2

HTML for HTTP Client Application

import todos from './todos.js'
const NEW_TODO_TEXT = 'A simple todo Element'
const printResult = (action, result) => {
  const time = (new Date()).toTimeString()
  const node = document.createElement('p')
  node.textContent = `${action.toUpperCase()}: ${JSON.stringify(result)} (${time})`
  document
    .querySelector('div')
    .appendChild(node)
}
const onListClick = async () => {
  const result = await todos.list()
  printResult('list todos', result)
}
const onAddClick = async () => {
  const result = await todos.create(NEW_TODO_TEXT)
  printResult('add todo', result)
}
const onUpdateClick = async () => {
  const list = await todos.list()
  const { id } = list[0]
  const newTodo = {
    id,
    completed: true
  }
  const result = await todos.update(newTodo)
  printResult('update todo', result)
}
const onDeleteClick = async () => {
  const list = await todos.list()
  const { id } = list[0]
  const result = await todos.delete(id)
  printResult('delete todo', result)
}
document
  .querySelector('button[data-list]')
  .addEventListener('click', onListClick)
document
  .querySelector('button[data-add]')
  .addEventListener('click', onAddClick)
document
  .querySelector('button[data-update]')
  .addEventListener('click', onUpdateClick)
document
  .querySelector('button[data-delete]')
  .addEventListener('click', onDeleteClick)
Listing 5-3

Main Controller for HTTP Client Application

In this controller, I didn’t use the HTTP client directly; instead, I wrapped the HTTP request in a todos model object. This kind of encapsulation is useful for a lot of reasons.

One of the reasons is testability. It’s possible to replace the todos object with a mock that returns a static set of data (also called a fixture) . In this way, I can test my controller in isolation.

Another reason is readability; model objects make your code more explicit.

Tip

Never directly use HTTP clients in controllers. Try to encapsulate these functions in model objects.

Listing 5-4 shows the todos model object.
import http from './http.js'
const HEADERS = {
  'Content-Type': 'application/json'
}
const BASE_URL = '/api/todos'
const list = () => http.get(BASE_URL)
const create = text => {
  const todo = {
    text,
    completed: false
  }
  return http.post(
    BASE_URL,
    todo,
    HEADERS
  )
}
const update = newTodo => {
  const url = `${BASE_URL}/${newTodo.id}`
  return http.patch(
    url,
    newTodo,
    HEADERS
  )
}
const deleteTodo = id => {
  const url = `${BASE_URL}/${id}`
  return http.delete(
    url,
    HEADERS
  )
}
export default {
  list,
  create,
  update,
  delete: deleteTodo
}
Listing 5-4

Todos Model Object

The signature of our HTTP client is http[verb](url, config) for verbs that don’t need a body, like GET or DELETE. For the other verbs, we can add the request body as a parameter, with this signature: http[verb](url, body, config).

There is no rule that forces your team to use this kind of public API for an HTTP client. Another option is to use http as a function and not as an object, adding the verb as a parameter: http(url, verb, body, config). Whatever you decide, try to keep it consistent.

Now that we have defined our HTTP client’s public contract, it’s time to look at the implementations.

XMLHttpRequest

The implementation in Listing 5-5 is based on XMLHttpRequest ( https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest ), which was W3C’s first attempt to create a standard way to make asynchronous HTTP requests.
const setHeaders = (xhr, headers) => {
  Object.entries(headers).forEach(entry => {
    const [
      name,
      value
    ] = entry
    xhr.setRequestHeader(
      name,
      value
    )
  })
}
const parseResponse = xhr => {
  const {
    status,
    responseText
  } = xhr
  let data
  if (status !== 204) {
    data = JSON.parse(responseText)
  }
  return {
    status,
    data
  }
}
const request = params => {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    const {
      method = 'GET',
      url,
      headers = {},
      body
    } = params
    xhr.open(method, url)
    setHeaders(xhr, headers)
    xhr.send(JSON.stringify(body))
    xhr.onerror = () => {
      reject(new Error('HTTP Error'))
    }
    xhr.ontimeout = () => {
      reject(new Error('Timeout Error'))
    }
    xhr.onload = () => resolve(parseResponse(xhr))
  })
}
const get = async (url, headers) => {
  const response = await request({
    url,
    headers,
    method: 'GET'
  })
  return response.data
}
const post = async (url, body, headers) => {
  const response = await request({
    url,
    headers,
    method: 'POST',
    body
  })
  return response.data
}
const put = async (url, body, headers) => {
  const response = await request({
    url,
    headers,
    method: 'PUT',
    body
  })
  return response.data
}
const patch = async (url, body, headers) => {
  const response = await request({
    url,
    headers,
    method: 'PATCH',
    body
  })
  return response.data
}
const deleteRequest = async (url, headers) => {
  const response = await request({
    url,
    headers,
    method: 'DELETE'
  })
  return response.data
}
export default {
  get,
  post,
  put,
  patch,
  delete: deleteRequest
}
Listing 5-5

HTTP Client with XMLHttpRequest

The core part of our HTTP client is the request method . XMLHttpRequest is an API defined in 2006, so it’s based on callbacks. We have the onload callback for a completed request, the onerror callback for any HTTP that ends with an error, and the ontimeout callback for a request that times out. There is no timeout by default, but you can change it by modifying the timeout property of the xhr object.

The HTTP client’s public API is based on promises ( https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise ). So the request method encloses the standard XMLHttpRequest request with a new Promise object. The public methods get, post, put, patch, and delete are wrappers around the request method (passing the appropriate parameters) to make the code more readable.

The following is the flow of an HTTP request with XMLHttpRequest (also see Figure 5-3).
  1. 1.

    Create a new XMLHttpRequest object (new XMLHttpRequest()).

     
  2. 2.

    Initialize the request to a specific URL (xhr.open(method, url)).

     
  3. 3.

    Configure the request (setting headers, timeout, etc.).

     
  4. 4.

    Send the request (xhr.send(JSON.stringify(body))).

     
  5. 5.
    Wait for the end of the request.
    1. a.

      If the request ends successfully, the onload callback is invoked.

       
    2. b.

      If the request ends with an error, the onerror callback is invoked.

       
    3. c.

      If the request times out, the ontimeout callback is invoked.

       
     
../images/476371_1_En_5_Chapter/476371_1_En_5_Fig3_HTML.png
Figure 5-3

Flow of an HTTP request with XMLHttpRequest

Fetch

Fetch is a new API created for access to remote resources. Its purpose is to provide a standard definition for many network objects, such as Request or Response. This way, these objects are interoperable with other APIs, such as ServiceWorker ( https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker ) and Cache ( https://developer.mozilla.org/en-US/docs/Web/API/Cache ).

To create a request, you need to use the window.fetch method , as you can see in Listing 5-6, which is the implementation of the HTTP client made with the Fetch API.
const parseResponse = async response => {
  const { status } = response
  let data
  if (status !== 204) {
    data = await response.json()
  }
  return {
    status,
    data
  }
}
const request = async params => {
  const {
    method = 'GET',
    url,
    headers = {},
    body
  } = params
  const config = {
    method,
    headers: new window.Headers(headers)
  }
  if (body) {
    config.body = JSON.stringify(body)
  }
  const response = await window.fetch(url, config)
  return parseResponse(response)
}
const get = async (url, headers) => {
  const response = await request({
    url,
    headers,
    method: 'GET'
  })
  return response.data
}
const post = async (url, body, headers) => {
  const response = await request({
    url,
    headers,
    method: 'POST',
    body
  })
  return response.data
}
const put = async (url, body, headers) => {
  const response = await request({
    url,
    headers,
    method: 'PUT',
    body
  })
  return response.data
}
const patch = async (url, body, headers) => {
  const response = await request({
    url,
    headers,
    method: 'PATCH',
    body
  })
  return response.data
}
const deleteRequest = async (url, headers) => {
  const response = await request({
    url,
    headers,
    method: 'DELETE'
  })
  return response.data
}
export default {
  get,
  post,
  put,
  patch,
  delete: deleteRequest
}
Listing 5-6

HTTP Client Based on the Fetch API

This HTTP client has the same public API as the one built with XMLHttpRequest: a request function wrapped with a method for each HTTP verb that we want to use. The code of this second client is much more readable because of window.fetch returning a Promise object, so we don’t need a lot of boilerplate code to transform the classic callback-based approach of XMLHttpRequest in a more modern promise-based one.

The promise returned by window.fetch resolves a Response object . We can use this object to extract the body of the response sent by the server. Depending on the format of the data received, there are different methods available; for example, text(), blob(), or json(). In our scenario, we always have JSON data, so it’s safe to use json().

Nevertheless, in a real-world application, you should use the right method accordingly with the Content-Type header. You can read the complete reference of all the objects of the Fetch API on the Mozilla Developer Network at https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch .

Axios

The last HTTP client that we are going to analyze is built with axios, a small open source library. The documentation and the source code is on GitHub ( https://github.com/axios/axios ).

The main characteristic that differentiates axios from the other approaches is that it works out-of-the-box for browsers and Node.js. Its API is based on promises, and thus it’s very similar to the Fetch API. In Listing 5-7, you can see the implementation of the HTTP client based on axios.
const request = async params => {
  const {
    method = 'GET',
    url,
    headers = {},
    body
  } = params
  const config = {
    url,
    method,
    headers,
    data: body
  }
  return axios(config)
}
const get = async (url, headers) => {
  const response = await request({
    url,
    headers,
    method: 'GET'
  })
  return response.data
}
const post = async (url, body, headers) => {
  const response = await request({
    url,
    headers,
    method: 'POST',
    body
  })
  return response.data
}
const put = async (url, body, headers) => {
  const response = await request({
    url,
    headers,
    method: 'PUT',
    body
  })
  return response.data
}
const patch = async (url, body, headers) => {
  const response = await request({
    url,
    headers,
    method: 'PATCH',
    body
  })
  return response.data
}
const deleteRequest = async (url, headers) => {
  const response = await request({
    url,
    headers,
    method: 'DELETE'
  })
  return response.data
}
export default {
  get,
  post,
  put,
  patch,
  delete: deleteRequest
}
Listing 5-7

HTTP Client Based on Axios

Among the three versions of the HTTP client, this is the easiest to read. The request method in this case rearranges the parameter to match the axios signature with the public contract.

Let’s Review Our Architecture

Let’s review our architecture. The three clients have the same public API. This characteristic of our architecture lets us change the library used for HTTP requests (XMLHttpRequest, Fetch, or axios) with minimal effort. JavaScript is a dynamically typed language, but all clients implement the HTTP client interface. Figure 5-4 shows a UML diagram that represents the relationship between the three implementations.
../images/476371_1_En_5_Chapter/476371_1_En_5_Fig4_HTML.jpg
Figure 5-4

UML diagram of HTTP client

We applied one of the most important principles of software design.

Program to an interface, not an implementation.

—Gang of Four

This principle, found in the Gang of Four’s book Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley Professional, 1994), is very important when working with libraries.

Imagine having a very large application with dozens of model objects that need to access network resources. If we use axios directly, without using our HTTP client, changing the implementation to the Fetch API would be a very expensive (and tedious) task. Using axios in our model objects means programming to an implementation (the library) and not to an interface (the HTTP client).

Caution

When using a library, always create an interface around it. It will be easier to change the library to a new one if you need to.

How to Choose the Right HTTP API

I talk about how to choose the “right” framework in the last chapter of this book. As I will explain more, there is no “right” framework. You can pick the “right” framework, but only for a “right” context.

So, for now, I will just point out the characteristics of XMLHttpRequest, the Fetch API, and axios from different perspectives.

Compatibility

If supporting Internet Explorer is important for your business, you have to rely on axios or XMLHttpRequest because the Fetch API works only on modern browsers.

Axios supports Internet Explorer 11, but if you need to work with older versions of Internet Explorer, you probably need to use XMLHttpRequest. Another option is to use a polyfill for the Fetch API ( https://github.github.io/fetch/ ), but I suggest this approach only if your team plans to remove support for Internet Explorer very soon.

Portability

Both the Fetch API and XMLHttpRequest work only on browsers. If you need to run your code in other JavaScript environments, such as Node.js or React Native, axios is a very good solution.

Evolvability

One of the most important features of the Fetch API is to provide a standard definition of network-related objects like Request or Response. This characteristic makes the Fetch API a very useful library if you want to evolve your codebase quickly, because it fits seamlessly with new APIs like the ServiceWorker API or the Cache API.

Security

Axios has a built-in protection system against cross-site request forgery, or XSRF ( https://en.wikipedia.org/wiki/Cross-site_request_forgery ). If you stick with XMLHttpRequest or the Fetch API, and you need to implement this kind of security system, I suggest that you examine axios unit tests about this topic ( https://github.com/axios/axios/blob/master/test/specs/xsrf.spec.js ).

Learning Curve

If your code is based on axios or the Fetch API, it will be easier for newcomers to grasp the bulk of its meaning. XMLHttpRequest may look a bit strange to junior developers because they may not be used to working with callbacks. In that scenario, be sure to wrap the internal API with a promise, as I did in my example.

Summary

In this chapter, you learned about the rise of AJAX and how it changed web development. Then you looked at three distinct ways to implement an HTTP client. The first two were completely frameworkless and based on standard libraries (XMLHttpRequest and the Fetch API). The third one was based on an open source project called axios. Finally, you saw the differences between them from different points of view.

In the next chapter, you learn how to create a frameworkless routing system, an essential element in a single-page application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.148.115.202