Using Axios with Vue 3 Composition API: A Practical Guide
Frontend • 5 min read

Using Axios with Vue 3 Composition API: A Practical Guide

Julius Fasema

Author

Modern Vue applications often need to communicate with backend APIs. Whether you are retrieving products, submitting forms, loading user profiles, or sending authentication requests, the frontend needs a reliable way to make HTTP requests.

Using Axios with Vue 3 Composition API: A Practical Guide

Modern Vue applications often need to communicate with backend APIs. Whether you are retrieving products, submitting forms, loading user profiles, or sending authentication requests, the frontend needs a reliable way to make HTTP requests.

One popular solution is Axios.

Axios is a promise-based HTTP client that makes it easy to communicate with REST APIs from JavaScript applications. When combined with Vue 3's Composition API, it provides a clean way to fetch, create, update, and delete data.

In this practical guide, we will build a small Vue 3 application that communicates with a REST API using Axios.

What We Are Building

We will create a simple Vue component that retrieves users from an API.

The flow will look like this:

Vue 3 Component
       |
       | Axios GET
       ↓
REST API
       |
       ↓
JSON Response
       |
       ↓
Vue Reactive State
       |
       ↓
Template

Our example API will be:

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

The API is expected to return something like:

[
    {
        "id": 1,
        "name": "John Doe",
        "email": "john@example.com"
    },
    {
        "id": 2,
        "name": "Mary Smith",
        "email": "mary@example.com"
    }
]

1. Create a Vue 3 Project

If you don't already have a Vue project, create one with Vite:

npm create vue@latest vue-axios-demo

Move into the project:

cd vue-axios-demo

Install the dependencies:

npm install

Start the development server:

npm run dev

You should see the application running at a URL similar to:

http://localhost:5173

2. Install Axios

Install Axios using npm:

npm install axios

You can verify the installation in package.json:

{
    "dependencies": {
        "axios": "^1.x"
    }
}

The exact version will depend on the version installed by npm.

3. Make Your First Axios Request

Let's create a simple Vue component.

<script setup>
import axios from 'axios'
import { ref, onMounted } from 'vue'

const users = ref([])

const getUsers = async () => {
    const response = await axios.get(
        'https://api.example.com/users'
    )

    users.value = response.data
}

onMounted(() => {
    getUsers()
})
</script>

<template>
    <div>
        <h1>Users</h1>

        <ul>
            <li
                v-for="user in users"
                :key="user.id"
            >
                {{ user.name }} - {{ user.email }}
            </li>
        </ul>
    </div>
</template>

There are several important things happening here.

4. Understanding ref()

Vue's Composition API uses ref() to create reactive values.

const users = ref([])

Initially:

users = []

After Axios retrieves the data:

users.value = response.data

Vue detects the change and automatically updates the template.

Inside JavaScript, we use:

users.value

But inside the Vue template, Vue automatically unwraps the ref:

{{ users }}

or:

v-for="user in users"

5. Understanding onMounted()

The onMounted() lifecycle hook runs after the component has been mounted.

onMounted(() => {
    getUsers()
})

This means:

Component created
       ↓
Component mounted
       ↓
getUsers()
       ↓
Axios request
       ↓
API response
       ↓
Update reactive state
       ↓
Vue updates UI

This is a common pattern when a page needs to load data as soon as it opens.

6. Handling Errors

Network requests can fail.

The server may be unavailable, the endpoint may be incorrect, or the user may have lost their internet connection.

Always handle Axios errors.

const users = ref([])
const error = ref(null)

const getUsers = async () => {

    try {

        const response = await axios.get(
            'https://api.example.com/users'
        )

        users.value = response.data

    } catch (err) {

        error.value = 'Unable to load users.'

        console.error(err)
    }
}

Then display the error:

<template>

    <div v-if="error">
        {{ error }}
    </div>

    <ul v-else>
        <li
            v-for="user in users"
            :key="user.id"
        >
            {{ user.name }}
        </li>
    </ul>

</template>

The user now receives useful feedback instead of seeing a blank page.

7. Add a Loading State

There is another problem.

An API request might take several seconds.

The application should tell the user that data is being loaded.

Create a reactive loading variable:

const loading = ref(false)

Then update it during the request:

const getUsers = async () => {

    loading.value = true
    error.value = null

    try {

        const response = await axios.get(
            'https://api.example.com/users'
        )

        users.value = response.data

    } catch (err) {

        error.value = 'Unable to load users.'

    } finally {

        loading.value = false
    }
}

The finally block is important because it runs whether the request succeeds or fails.

In the template:

<template>

    <div v-if="loading">
        Loading users...
    </div>

    <div v-else-if="error">
        {{ error }}
    </div>

    <ul v-else>
        <li
            v-for="user in users"
            :key="user.id"
        >
            {{ user.name }}
        </li>
    </ul>

</template>

Now the interface has three states:

Loading
   ↓
Success

or

Loading
   ↓
Error

This simple pattern is useful in almost every API-driven Vue application.

8. Making a POST Request

Axios isn't only used for GET requests.

You can use it to create resources with POST.

Suppose our API provides:

POST /users

We can send:

const createUser = async () => {

    const user = {
        name: 'Julius Fasema',
        email: 'julius@example.com'
    }

    try {

        const response = await axios.post(
            'https://api.example.com/users',
            user
        )

        console.log(response.data)

    } catch (err) {

        console.error(err)
    }
}

The second argument to axios.post() is the request body.

Axios automatically serializes the JavaScript object as JSON for a typical JSON API request.

The server receives:

{
    "name": "Julius Fasema",
    "email": "julius@example.com"
}

9. Sending Form Data

Suppose you have a registration form.

<script setup>
import { ref } from 'vue'
import axios from 'axios'

const name = ref('')
const email = ref('')

const register = async () => {

    try {

        const response = await axios.post(
            'https://api.example.com/register',
            {
                name: name.value,
                email: email.value
            }
        )

        console.log(response.data)

    } catch (error) {

        console.error(error)
    }
}
</script>

<template>

    <form @submit.prevent="register">

        <input
            v-model="name"
            placeholder="Name"
        />

        <input
            v-model="email"
            type="email"
            placeholder="Email"
        />

        <button type="submit">
            Register
        </button>

    </form>

</template>

v-model keeps the form fields synchronized with the reactive variables.

When the user submits the form, Axios sends the values to the backend.

10. PUT Requests

PUT can be used to update a resource.

For example:

const updateUser = async (id) => {

    const data = {
        name: 'Updated Name',
        email: 'updated@example.com'
    }

    const response = await axios.put(
        `https://api.example.com/users/${id}`,
        data
    )

    console.log(response.data)
}

If id is 10, the request becomes:

PUT /users/10

11. DELETE Requests

Deleting a resource is equally simple:

const deleteUser = async (id) => {

    try {

        await axios.delete(
            `https://api.example.com/users/${id}`
        )

        users.value = users.value.filter(
            user => user.id !== id
        )

    } catch (error) {

        console.error(error)
    }
}

The API receives:

DELETE /users/10

After a successful response, we remove the user from the local Vue state.

12. Using Axios Query Parameters

Suppose your API supports searching:

GET /users?search=julius

Axios allows you to provide query parameters using params.

const searchUsers = async (search) => {

    const response = await axios.get(
        'https://api.example.com/users',
        {
            params: {
                search: search
            }
        }
    )

    users.value = response.data
}

Axios generates:

/users?search=julius

You can also send multiple parameters:

const response = await axios.get(
    'https://api.example.com/products',
    {
        params: {
            search: 'laptop',
            page: 1,
            size: 20
        }
    }
)

The resulting request could be:

/products?search=laptop&page=1&size=20

This is cleaner than manually constructing the query string.

13. Creating an Axios Instance

As an application grows, you don't want to repeat the API URL everywhere.

Avoid doing this throughout your application:

axios.get('https://api.example.com/users')
axios.get('https://api.example.com/products')
axios.post('https://api.example.com/orders')

Instead, create an Axios instance.

Create:

src/
└── services/
    └── api.js

Then:

import axios from 'axios'

const api = axios.create({
    baseURL: 'https://api.example.com',
    headers: {
        'Content-Type': 'application/json'
    }
})

export default api

Now your components can use:

import api from '@/services/api'

const response = await api.get('/users')

Instead of:

axios.get(
    'https://api.example.com/users'
)

This makes the code cleaner and easier to maintain.

14. Using Environment Variables

The API URL will often be different between development and production.

For example:

Development:
http://localhost:8080/api

Production:
https://api.example.com/api

Don't hard-code these values throughout your application.

Create:

.env.development

with:

VITE_API_URL=http://localhost:8080/api

Then:

.env.production

could contain:

VITE_API_URL=https://api.example.com/api

Update your Axios instance:

import axios from 'axios'

const api = axios.create({
    baseURL: import.meta.env.VITE_API_URL,
    headers: {
        'Content-Type': 'application/json'
    }
})

export default api

Now Vite selects the appropriate API URL based on the environment.

15. Organizing API Services

As your application grows, it is useful to separate API operations into service files.

For example:

src/
├── services/
│   ├── api.js
│   ├── userService.js
│   ├── productService.js
│   └── orderService.js

userService.js:

import api from './api'

export const getUsers = () => {
    return api.get('/users')
}

export const getUser = (id) => {
    return api.get(`/users/${id}`)
}

export const createUser = (data) => {
    return api.post('/users', data)
}

export const updateUser = (id, data) => {
    return api.put(`/users/${id}`, data)
}

export const deleteUser = (id) => {
    return api.delete(`/users/${id}`)
}

The Vue component can then become much simpler:

import { getUsers } from '@/services/userService'

const loadUsers = async () => {

    try {

        const response = await getUsers()

        users.value = response.data

    } catch (error) {

        console.error(error)
    }
}

The component doesn't need to know the API URL or how the HTTP request is constructed.

16. Axios Interceptors

Interceptors are useful when you need to modify requests or responses globally.

For example, suppose your API requires a JWT token.

Instead of manually adding the token to every request, you can use a request interceptor.

api.interceptors.request.use(
    (config) => {

        const token = localStorage.getItem('token')

        if (token) {
            config.headers.Authorization =
                `Bearer ${token}`
        }

        return config
    }
)

Now every request made through api can automatically include:

Authorization: Bearer <token>

This is especially useful for authenticated applications.

17. Handling 401 Responses

You can also use a response interceptor.

api.interceptors.response.use(
    response => response,

    error => {

        if (error.response?.status === 401) {

            console.log('Authentication required')

        }

        return Promise.reject(error)
    }
)

Now the application has one central place for handling authentication failures.

In a real application, this could trigger:

401 response
     ↓
Clear authentication state
     ↓
Redirect to login

18. A Complete Vue 3 Example

Let's combine the concepts.

<script setup>
import { ref, onMounted } from 'vue'
import api from '@/services/api'

const users = ref([])
const loading = ref(false)
const error = ref(null)

const getUsers = async () => {

    loading.value = true
    error.value = null

    try {

        const response = await api.get('/users')

        users.value = response.data

    } catch (err) {

        console.error(err)

        error.value =
            'Unable to load users.'

    } finally {

        loading.value = false
    }
}

onMounted(() => {
    getUsers()
})
</script>

<template>

    <section>

        <h1>Users</h1>

        <p v-if="loading">
            Loading users...
        </p>

        <p v-else-if="error">
            {{ error }}
        </p>

        <ul v-else>
            <li
                v-for="user in users"
                :key="user.id"
            >
                <strong>{{ user.name }}</strong>
                <br>
                {{ user.email }}
            </li>
        </ul>

    </section>

</template>

This is a simple but realistic pattern for a Vue 3 application.

19. Recommended Project Structure

For a small application:

src/
├── components/
├── views/
├── services/
│   ├── api.js
│   ├── userService.js
│   └── productService.js
├── router/
├── stores/
├── App.vue
└── main.js

For larger applications, the structure can be organized around features rather than simply technical layers.

The important principle is to avoid putting all API communication directly into every component.

20. Axios vs Fetch

Modern browsers already provide the fetch() API.

So why use Axios?

Both can make HTTP requests.

Axios provides conveniences such as:

  • Simple request syntax

  • Automatic JSON transformation

  • Request and response interceptors

  • Convenient configuration

  • Centralized Axios instances

  • Request timeout configuration

  • Consistent error handling

  • Easy integration with authentication headers

For example, Axios:

const response = await api.get('/users')
const users = response.data

With Fetch:

const response = await fetch('/users')
const users = await response.json()

Neither is universally "better." The right choice depends on the project and team. Axios becomes particularly convenient when an application needs centralized HTTP configuration, interceptors, or multiple API clients.

21. Common Mistakes to Avoid

Putting API URLs everywhere

Avoid:

axios.get('https://api.example.com/users')

throughout multiple components.

Use a centralized Axios instance.

Ignoring loading states

Don't leave users staring at an empty page while a request is running.

Provide feedback:

Loading...

Ignoring errors

Every network request can fail.

Use:

try {
    // request
} catch (error) {
    // handle error
}

Storing sensitive credentials carelessly

Be careful about storing authentication information in browser storage.

Understand the security implications of localStorage, cookies, XSS, CSRF, and your authentication architecture before deciding where tokens should live.

Putting too much logic in components

Instead of making a large component responsible for every API operation, move API communication into service modules.

For example:

Component
    ↓
userService
    ↓
Axios
    ↓
Backend API

This separation makes the application easier to maintain.

Practical Checklist

Before considering your Axios integration complete, check:

  • Axios is installed

  • API URLs are configured through environment variables

  • An Axios instance is used

  • API operations are separated into services

  • Loading states are handled

  • Errors are handled

  • Request parameters are handled correctly

  • Authentication headers are centralized where appropriate

  • 401 responses are handled

  • Components are not overloaded with API logic

Conclusion

Axios and Vue 3's Composition API work well together for building applications that communicate with REST APIs.

The basic pattern is straightforward:

ref()
  ↓
onMounted()
  ↓
Axios
  ↓
REST API
  ↓
response.data
  ↓
Reactive state
  ↓
Vue template

For small applications, a simple Axios call inside a component may be enough.

As the application grows, however, it is better to introduce an Axios instance, environment variables, API service modules, interceptors, authentication handling, and consistent error management.

The result is a frontend that is easier to maintain and a clear separation between the Vue user interface and the backend API.

Tags: Vue 3, Vue.js, Axios, Composition API, JavaScript, REST API, API Integration, HTTP Requests, Vite, Frontend Development, Vue Composition API, Web Development