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