Fetch API Components with Vue 3 Composition API

Part 4 of 4 in Using Fetch API with VueJS
Dan Pastori avatar
Dan Pastori November 29th, 2021

One 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.

Support future content

The Ultimate Guide to Building APIs and Single-Page Applications with Laravel + VueJS + Capacitor book cover.

Psst... any earnings that we make off of our book is being reinvested to bringing you more content. If you like what you read, consider getting our book or get sweet perks by becoming a sponsor.

Written By Dan

Dan Pastori avatar Dan Pastori

Builder, creator, and maker. Dan Pastori is a Laravel certified developer with over 10 years experience in full stack development. When you aren't finding Dan exploring new techniques in programming, catch him at the beach or hiking in the National Parks.