Fetch API Components with Vue 3 Composition API
Part 4 of 4 in Using Fetch API with VueJSOne of the biggest pain points I see between using the Fetch API and Axios is the way Fetch is configured out of the box. I love the power, but Axios comes with some default config and a way to make it easy to set global settings. I want to see if I can abstract some of the settings that are generally used across all API requests and make them reusable with the Fetch API. I’m going to structure this reusability similar to building an API wrapper with axios. The main difference (besides using Fetch instead of Axios), is we are going to implement these methods using the new Vue 3 Composition API!
Step 1: Plan our API
Let’s keep it really simple for this tutorial and do an example of sending data to the API and retrieving data from the API. We will use the https://api.roastandbrew.coffee/api/v1/brew-methods end point. We will create a method that performs a GET
request and loads all of the brew methods available and a method that sends POST
data to the endpoint to create a brew method.
With that being said, we will need some standardized settings for our Fetch API wrapper. These will be:
- token → The authorization token we will add to the headers
- headers → The default headers represented by the Header() object
- baseURL → The base URL of our API
We can then set up our config and re-use it with all of our API requests.
Step 2: Create Directories and Config
The first step I take when making reusable API wrapper modules is to create a directory to house our implementation. This is very similar to Building an API Wrapper with VueJS and Axios. The only difference is with our Fetch API implementation, we will create a small config object that we can control in one area that has some defaults already defined for us.
Since we are working with Brew Methods, let’s first create an api/resources/BrewMethods.js
file. I usually put all API resources in the root level of my source javascript:
export default {
index( ){
},
store( data ){
}
}
For now these are just the place holder methods we will be implementing. We can leave it that way for now.
Next, let’s create our api/config.js
file:
export const APISettings = {
token: '',
headers: new Headers({
'Accept': 'application/json'
}),
baseURL: 'https://api.roastandbrew.coffee/api/v1',
}
This is just a simple JSON object that just houses a few of our global values we will be implementing in all of our requests. The token
field will hold our access token. WARNING!! If you are working with a third party API and you are wrapping it’s resources for all users of your application to use. DO NOT STORE YOUR TOKEN HERE! This is meant to be a personal access token. Secret tokens should never be stored in javascript. It will lead to the ability for users to inspect and grab the token and act on your app’s behalf!
Working with Headers()
The next headers
field allows us to set default headers that we will use in each request. The field instantiates the Headers interface (https://developer.mozilla.org/en-US/docs/Web/API/Headers). Since our API returns JSON, we default the Accept: 'application/json'
header. When we implement our API wrapper, we will add other headers to handle authorization and such.
Finally, our baseURL
is the base of the endpoints we will be calling. This is a nice little feature so if the API updates, we can just switch to a different API version with ease and all of our requests will work.
Step 3: Include Config in Wrapper
Now that we have our config set up, we need to include it in every endpoint wrapper so we can re-use the settings. To do this, add the following to the top of the file:
import { APISettings } from '../config.js';
Now we have access to our config from within our API Wrapper. Let’s build out each of the endpoints, then we can include them in our components with the Vue 3 Composition API.
Step 4: Build out Reusable API Endpoints
The biggest goal with this is to store each endpoint request in a way that we can use it in whatever component we need. Why? So if we need to make changes to the endpoint we can update it in one place and it’s fixed in our entire application.
Let’s shell out our GET
request to load all of the Brew Methods. To do this, add the following code to your BrewMethods.js
file:
index( ){
return fetch( APISettings.baseURL + '/brew-methods', {
method: 'GET',
headers: APISettings.headers
} )
.then( function( response ){
if( response.status != 200 ){
throw response.status;
}else{
return response.json();
}
});
},
Now, when we call BrewMethods.index()
we will return the fetch()
method which in turn returns a promise that we can listen to. Two things to notice in our method call. First, we call APISettings.baseURL + '/brew-methods'
as our endpoint. This appends the baseURL we set up in Step 3 to build out our endpoint.
Next, we reference the same config with the headers: APISettings.headers
setting. All of our headers that are in the global config are appended to the request. In the next request, we will append our authorization header. You can do that in this request as well, but we are just building up the settings as we go.
We then do one consistent “pre-process” with the returning of the request data. That is, upon completion, if the status is not 200
(successful response), we throw response.status
which throws an error with the code that was returned. This way we can handle that error in a catch
callback. If the request was successful, we return response.json()
which decodes the JSON and returns the promise we can handle in our method.
Let’s add our POST
request here while we are at it:
store( data ){
APISettings.headers.set('Content-Type', 'multipart/form-data');
APISettings.headers.set('Authorization', 'Bearer '+APISettings.token);
return fetch( APISettings.baseURL + '/brew-methods', {
method: 'POST',
headers: APISettings.headers,
body: data
} )
.then( function( response ){
if( response.status != 201 ){
throw response.status;
}else{
return response.json();
}
});
}
The major differences between sending the request to the server and getting data from the server are the headers, and the way the body is encoded. Right away, we set two extra headers. The first let’s the server know that the Content-Type
is multipart/form-data
. This header allows us to send files to the server. The second header Authorization
sends the token we set in our config as the Authorization: Bearer
token. This allows properly authenticated users to create a resource.
Ever other part of this request should look very similar to uploading files. We do the same pre-processing where we throw an error if the status is not 201
which is “Resource Created”. We now have a reusable Fetch API module. Let’s implement these re-usable endpoints through the Vue 3 Composition API.
Step 5: Set Up Composition API to Use API Module
Let’s say we have a component that handles the Brew Method resource in our application ( check out our GitHub for an example ). The first thing we need to do is import our module into our component:
import BrewMethodsAPI from '../api/resources/BrewMethods.js';
Now that we have the API module imported into our component, let’s use the Vue 3 Composition API to set up what we need from the module.
First, we will look at the setup()
hook. This is where the magic takes place with the Composition API. We can set up our component to use pieces of other modules allowing us to easily re-use code. In past tutorials, I’ve done this by just making local methods that call our module’s methods. This works too! However, if you want a standardized validation before the form is submitted and other helper methods, you can easily re-write a ton of code and it’s not that re-usable. With the Composition API, I can store all of my validator methods with the request so it’s all localized!
The setup() Method
Our setup()
method looks like this:
setup( ){
const brewMethods = ref({});
const loadBrewMethods = async() => {
brewMethods.value = await BrewMethodsAPI.index();
};
const form = {
method: '',
icon: ''
};
const saveBrewMethod = async() => {
let formData = new FormData();
formData.append('method', form.method);
formData.append('icon', form.icon);
await BrewMethodsAPI.store( formData )
}
return {
brewMethods,
loadBrewMethods,
form,
saveBrewMethod
}
},
For those who have not worked with the composition API, this may look a little bit unique. Before we dive in, add the following line to the imports on your component:
import { ref } from 'vue';
Don’t worry, we will break this all down. Let’s start at the top of the method:
const brewMethods = ref({});
const loadBrewMethods = async() => {
brewMethods.value = await BrewMethodsAPI.index();
};
Right away we initialize a brewMethods
variable to ref({})
. What this does is create a reactive piece of data using the ref()
helper. Right now, we just initialize it to an empty object. Vue will now make whatever is set to the value reactive so we can work with it accordingly.
Building our async() Function
Next, we define const loadBrewMethods
and set it equal to an async()
function. Since the Fetch API returns a promise, we can clean up the request by using async/await
. Inside the async
wrapping, we make our call to BrewMethodsAPI.index()
. What is returned from this method call is set to brewMethods.value
which will reactive for use within our component.
At the very end of our setup()
method, we return brewMethods
and loadBrewMethods
(as well as some other variables we will talk about next).
By returning the brewMethods
variable and loadBrewMethods
function, we can now access that from within our component. Any call to this.loadBrewMethods()
will call our reusable module!
Adding the request to create a brew method is very similar in the setup()
method:
const form = {
method: '',
icon: ''
};
const saveBrewMethod = async() => {
let formData = new FormData();
formData.append('method', form.method);
formData.append('icon', form.icon);
await BrewMethodsAPI.store( formData )
}
The big thing I’d like to point out is the const form
. We return the form
from our setup()
method. This allows us to use v-model
directives and bind directly to the pieces of data returned.
The other difference is we are using FormData()
to send the data to our endpoint. That’s why we instantiate a new FormData()
object and send the data from our form this way. This allows us to send a file. For more information about uploading files with Fetch API, check out File Uploads using Fetch API and VueJS.
Conclusion
Abstracting these requests into reusable components saves so much time and maintenance. Using the Vue 3 Composition API makes this a lot easier as well. If you have any questions, reach out on the community, I’d be happy to help!
If you want to see how all of this works together, check out our GitHub repo. You can see our entire component there.