In this post, I will share my thought on how I like to structure methods on how to get data from your REST API backend. The idea here is to show the basics of my idea, and then you should add, remove, and adopt the code to your specific needs.
This method will also be useful in what ever framework or platform you are in like React, Angular, Vue, or even NodeJS (with a few polyfills, for example fetch...)!
Oh, and a little heads up. We are going object-oriented programming. So a basic understanding of javascript classes and the fetch api would be good before you keep going.
The end game
In the end, we will be able to request data from our backend like this to assign a users variable:
users = await backend.users.get()
Instead of something like this:
const res = await fetch('/users', {
headers: {
Authorization: '********',
lang: 'en'
}
})
if(!res.ok) throw new Error(res.statusText)
users = await res.json()
Motivation
So why go through this process? Well, first of all, it will make your code easier to read. You will get all the code from fetch hidden behind explanatory method calls. Like backend.get.users()
, that makes sense and is short.
Sure you could extract the logic into a function named getUsers()
and make the fetch call in there. But then here is the next benefit: don't repeat your self. Without a good structure for your back end requests, you will definitely repeat your self. Setting authorization and other headers in multiple places, or just to see if the fetch call is ok
everywhere...
You could also move this code outside into a library for use in your web apps and Node services without a hassle.
Lets get started
So we will start of by making our own "mini, mini, mini version" of Axios (or insert name of http client here):
class HttpClient {
constructor(options = {}) {
this._baseURL = options.baseURL || "";
this._headers = options.headers || {};
}
}
We start things off with the constructor, we will accept two options when instantiating the class:
Base URL will be used to construct the URL. Later we will use a get method like this get('/users')
and if we defined the baseURL
with https://jsonplaceholder.typicode.com
the request URL will be https://jsonplaceholder.typicode.com/users
Headers will be a default set of headers that will be sent with every request.
If you think, "what is with the _ before property names?" Well, I intend these properties to be private. Meant only to be used inside this (or inherited) classes. Not from anywhere outside in the actual app code. Soon there will be a syntax for this as well that implements the behavior, check this out
We should probably add some way of setting headers as well:
setHeader(key, value) {
this._headers[key] = value;
return this;
}
At the end of the setHeader method I added return this
. This is added so we can chain
method calls. For example when instantiating the HttpClient class:
const httpClient = new HttpClient({baseURL: 'xxx'})
.setBasicAuth("user", "pass")
.setHeader("lang", "en")
Now in the example above I used another method setBasicAuth
. I will skip that for now, but at the end of this post, you will find some inspiration for more properties and things you can add to the client.
Lets make requests!
This will be done in two steps. First of all we will define our own wrapper function for fetch, and then we will make separate methods for get/post/put/delete/patch
:
async _fetchJSON(endpoint, options = {}) {
const res = await fetch(this._baseURL + endpoint, {
...options,
headers: this._headers
});
if (!res.ok) throw new Error(res.statusText);
if (options.parseResponse !== false && res.status !== 204)
return res.json();
return undefined;
}
So this wrapper function just makes fetch behave a little more like I want it to do in this specific use case. Like the fact that fetch does not throw on bad requests.
The first argument is just the endpoint (string), and if we set the baseURL
option it will be relative to that.
And the options argument is just the fetch Request
object we can add additional properties to. It can be empty, but more information about available properties is found here.
Oh! But I do append the parseResponse
options to this argument to tell whether the response should be parsed to JSON or not. For most of my use cases, I want to opt-out of the parsing. So if left blank the parsing is done if not the API explicitly states No Content
.
You could argue that we could check the content length or some other stuff, but what is good is that if I do require a response and I say I wanted it parsed. This method will throw if I do not get the response. So it will blow up here instead of in my application later where it might take me longer to find the cause.
Now lets expose some ways of making request. This should hopefully be straight forward:
get(endpoint, options = {}) {
return this._fetchJSON(
endpoint,
{
...options,
method: 'GET'
}
)
}
post(endpoint, body, options = {}) {
return this._fetchJSON(
endpoint,
{
...options,
body: JSON.stringify(body),
method: 'POST'
}
)
}
delete(endpoint, options = {}) {
return this._fetchJSON(
endpoint,
{
parseResponse: false,
...options,
method: 'DELETE'
}
)
}
/** AND SO ON */
We simply call our _fetchJSON
method and set some options to make the HTTP method match our method name, and maybe set a body correct so that is taken care of.
Now we could just do some API calls:
const httpClient = new HttpClient({baseURL: 'https://example.com'})
.setHeader('lang', 'sv')
const users = await httpClient.get('/users')
One step further: The API client
We have done a lot! This client is our own "mini, mini, mini-version" of Axios. That we easily can extend with whatever parameters, options, or functions we would need.
But I want to take it one step further, I want to define our back end API with easy to call methods. Like I mentioned in the beginning. Now we can take one of two approaches. We could just add more methods to the HttpClient directly, and keep on working.
But, this class does serve its purpose now, right? It can work on its own, and be useful that way. So what if we let the HttpClient class be our base class and we can inherit that one to create our ApiClient class.
This way we can make other HttpClients for talking to other services by using the HttpClient class directly. And talking to our backend with the ApiClient class, which just adds to the existing functionality.
Inheriting the HttpClient class would look like this:
import HttpClient from "./http-client"
class ApiClient extends HttpClient {
constructor(baseURL, langCode) {
super({
baseURL,
headers: {
lang: langCode
}
});
}
get users() {
return {
get: () => this.get("/users"),
delete: (id) => this.delete(`/users/${id}`),
create: (user) => this.post("/users", user),
update: (user) => this.put(`/users/${user.id}`, user)
};
}
}
export default ApiClient
Well, that was quite fast. We just added our little flavor to the constructor and we could simple and quickly define our endpoints.
And adding additional endpoints is now really simple and reliable.
Take it further
Now, this was a quick way to add basic functionality and then extending this to make the specific client.
The idea here is to make the base as simple as possible and then add every feature you need instead of bringing in the full capacity of an external library upfront.
Some things you could do next, if applicable to your needs of course:
Add helpers to authenticate if you do not rely on cookies
For example, if you need basic authentication:
setBasicAuth(username, password) {
this._headers.Authorization = `Basic ${btoa(`${username}:${password}`)}`
return this
}
Just remember that btoa is not available globally in NodeJS. But just polyfill it and you will be good to go.
And for a bearer auth:
setBearerAuth(token) {
this._headers.Authorization = `Bearer ${token}`
return this
}
Make the functions generic if you are on typescript
I do like typescript, 90% of all my code is in typescript. When building this for typescript add a generic return type to the functions, and for your post methods you should type the expected body:
_fetchJSON<T = any>(endpoint: string, options: RequestInit = {}):Promise<T>{ /**/ }
I usually do a little hack when the body is not parsed and say return undefined as any
to just get Typescript to not complain. If you expect undefined T
should be undefined
and you are good to go.
And in your API client for a post request like this:
users = {
post: (user:IUser) => this.post<IUser>('/users', user)
}
Add methods to any applicable header your API expects/can use
For example at work we have a header to include or exclude empty values in the response (to save some time transferring large collections)
includeEmptyAndDefault(shouldInclude) {
if(shouldInclude) {
this._headers.IncludeEmptyAndDefault = 1
} else {
this._headers.IncludeEmptyAndDefault = 0
}
return this
}
Build a package
Use Rollup or tsdx, if you are into typescript, if you want to do a separate package. This way the API client can be used as a module in other projects as well. That can be great for you, and your customers, to make things happen fast.
But as I said, only add what you need. If you have any thoughts please share them in the comments and recommend me any patterns you like if this was not your cup of tea.
Cover photo by Florian Krumm on Unsplash
Top comments (16)
Great post. This makes me remember the functionalitys of packages HttpClient of PHP. This is very useful for simplify of the handle http request's. Thanks for the tutorial.
Cool! My experience with PHP is limited though. The inspiration is from my background doing C# and their httpclient class. Glad you liked it!
The C# HTTPClient is exactly what I was thinking about while reading this. Great post!
Its always good to look at other languages some times to find inspiration. Thank you!
Nice post
Thanks, glad you enjoyed it!
Nice way to use classes in JS, with
extends
Yeah, when looking at docs for classes all examples of extending is these things like animal extending to dog or car extending to Honda. So it is Nice with a more "real world" context for extending.
Oh, good tutorial. You save a lot my time 👍
Awesome! More time spent doing fun stuff
Nice article!
Thank you!
Nice article! The HttpClient in your example code is using "POST" for the the HTTP method when calling the "put" function on it. Great little wrapper around fetch, though. :)
Oh! Always good with an extra pair of eyes on your code, thank you :-)
Nice and cool
Thanks!