Skip to main content

Vue.js REST Requests

Introduction

In modern web development, most Vue.js applications need to communicate with backend services to fetch, create, update, or delete data. REST (Representational State Transfer) is a popular architectural style for designing networked applications, and RESTful APIs provide a standardized way for your Vue.js applications to interact with servers.

In this tutorial, you'll learn:

  • What REST requests are and their common types
  • How to make REST requests in Vue.js using the native Fetch API
  • How to use Axios, a popular HTTP client library, with Vue.js
  • Best practices for organizing API calls in your Vue.js applications
  • Error handling and loading states

Understanding REST Requests

REST (Representational State Transfer) is an architectural style that uses HTTP requests to access and manipulate data. RESTful APIs typically use standard HTTP methods to perform different operations:

HTTP MethodPurposeExample
GETRetrieve dataFetch a list of users
POSTCreate new dataCreate a new user
PUTUpdate existing data (replace)Update all user information
PATCHUpdate existing data (partial)Update just a user's email
DELETERemove dataDelete a user

Making REST Requests in Vue.js

There are several ways to make REST requests in Vue.js applications:

  1. Using the built-in Fetch API
  2. Using the Axios library
  3. Using other HTTP client libraries

Let's explore each approach:

Using the Fetch API

The Fetch API is built into modern browsers and provides a simple interface for making HTTP requests.

Basic GET Request

javascript
export default {
data() {
return {
users: [],
loading: false,
error: null
}
},
methods: {
async fetchUsers() {
this.loading = true;
this.error = null;

try {
const response = await fetch('https://api.example.com/users');

if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}

this.users = await response.json();
} catch (error) {
this.error = error.message;
console.error('Error fetching users:', error);
} finally {
this.loading = false;
}
}
},
mounted() {
this.fetchUsers();
}
}

POST Request Example

javascript
async createUser(userData) {
this.loading = true;
this.error = null;

try {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});

if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}

const newUser = await response.json();
this.users.push(newUser);
return newUser;
} catch (error) {
this.error = error.message;
console.error('Error creating user:', error);
} finally {
this.loading = false;
}
}

Using Axios

Axios is a popular HTTP client library that works in both the browser and Node.js. It provides a more feature-rich API than Fetch, with better error handling and request configuration.

First, install Axios:

bash
npm install axios

Basic Setup with Axios

You can create an Axios instance with a base URL and common headers:

javascript
// api/index.js
import axios from 'axios';

const apiClient = axios.create({
baseURL: 'https://api.example.com',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
timeout: 10000 // 10 seconds
});

export default apiClient;

GET Request with Axios

javascript
// UserService.js
import apiClient from './api';

export default {
getUsers() {
return apiClient.get('/users');
},
getUser(id) {
return apiClient.get(`/users/${id}`);
}
}

Using the service in a component:

javascript
import UserService from '@/services/UserService';

export default {
data() {
return {
users: [],
loading: false,
error: null
}
},
methods: {
async fetchUsers() {
this.loading = true;
this.error = null;

try {
const response = await UserService.getUsers();
this.users = response.data;
} catch (error) {
this.error = error.response ? error.response.data.message : error.message;
console.error('Error fetching users:', error);
} finally {
this.loading = false;
}
}
},
mounted() {
this.fetchUsers();
}
}

POST Request with Axios

javascript
// UserService.js (extended)
import apiClient from './api';

export default {
// ... previous methods
createUser(userData) {
return apiClient.post('/users', userData);
},
updateUser(id, userData) {
return apiClient.put(`/users/${id}`, userData);
},
deleteUser(id) {
return apiClient.delete(`/users/${id}`);
}
}

Using POST in a component:

javascript
async submitUser() {
this.submitting = true;

try {
const response = await UserService.createUser(this.userForm);
this.users.push(response.data);
this.resetForm();
this.$emit('user-created', response.data);
} catch (error) {
this.formError = error.response ? error.response.data.message : error.message;
} finally {
this.submitting = false;
}
}

Organizing API Calls in Vue.js Applications

For larger applications, it's best to organize your API calls into service modules. This helps with:

  1. Code reusability
  2. Easier testing
  3. Centralized API configuration
  4. Simplified component code

Here's a recommended structure:

src/
├── api/
│ ├── index.js # Axios instance and config
│ └── interceptors.js # Request/response interceptors
├── services/
│ ├── UserService.js # User-related API calls
│ ├── ProductService.js # Product-related API calls
│ └── AuthService.js # Authentication-related API calls

Example API Module Structure

javascript
// api/interceptors.js
import apiClient from './index';

// Add a request interceptor
apiClient.interceptors.request.use(
config => {
// Do something before request is sent
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => {
// Do something with request error
return Promise.reject(error);
}
);

// Add a response interceptor
apiClient.interceptors.response.use(
response => {
// Any status code within the range of 2xx triggers this function
return response;
},
error => {
// Any status codes outside the range of 2xx trigger this function
if (error.response && error.response.status === 401) {
// Handle unauthorized errors (e.g., redirect to login)
}
return Promise.reject(error);
}
);

export default apiClient;

Practical Example: Building a Todo App with REST

Let's build a simple Todo application that demonstrates CRUD operations with a REST API:

javascript
// api/index.js
import axios from 'axios';

export default axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
headers: {
'Content-Type': 'application/json'
}
});
javascript
// services/TodoService.js
import apiClient from '@/api';

export default {
getTodos() {
return apiClient.get('/todos?_limit=10');
},
getTodo(id) {
return apiClient.get(`/todos/${id}`);
},
createTodo(todo) {
return apiClient.post('/todos', todo);
},
updateTodo(id, todo) {
return apiClient.put(`/todos/${id}`, todo);
},
deleteTodo(id) {
return apiClient.delete(`/todos/${id}`);
}
};

Now let's create a Todo component:

html
<template>
<div class="todos">
<div v-if="loading" class="loading">Loading todos...</div>
<div v-if="error" class="error">{{ error }}</div>

<div v-if="!loading && !error">
<!-- Todo Form -->
<form @submit.prevent="addTodo" class="todo-form">
<input
v-model="newTodo.title"
placeholder="What needs to be done?"
required
/>
<button type="submit" :disabled="submitting">Add</button>
</form>

<!-- Todo List -->
<ul class="todo-list">
<li v-for="todo in todos" :key="todo.id" :class="{ completed: todo.completed }">
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo)"
/>
<span class="todo-title">{{ todo.title }}</span>
<button @click="removeTodo(todo.id)" class="delete">×</button>
</li>
</ul>
</div>
</div>
</template>

<script>
import TodoService from '@/services/TodoService';

export default {
name: 'TodoList',
data() {
return {
todos: [],
newTodo: {
title: '',
completed: false,
userId: 1
},
loading: false,
submitting: false,
error: null
}
},
methods: {
async fetchTodos() {
this.loading = true;
try {
const response = await TodoService.getTodos();
this.todos = response.data;
} catch (error) {
this.error = "Failed to load todos. Please try again.";
console.error(error);
} finally {
this.loading = false;
}
},
async addTodo() {
if (!this.newTodo.title.trim()) return;

this.submitting = true;
try {
const response = await TodoService.createTodo(this.newTodo);
this.todos.unshift(response.data);
this.newTodo.title = '';
} catch (error) {
alert('Failed to add todo');
console.error(error);
} finally {
this.submitting = false;
}
},
async toggleTodo(todo) {
try {
const updatedTodo = { ...todo, completed: !todo.completed };
const response = await TodoService.updateTodo(todo.id, updatedTodo);

// Update the todo in our list
const index = this.todos.findIndex(t => t.id === todo.id);
this.todos[index] = response.data;
} catch (error) {
alert('Failed to update todo');
console.error(error);
}
},
async removeTodo(id) {
try {
await TodoService.deleteTodo(id);
this.todos = this.todos.filter(todo => todo.id !== id);
} catch (error) {
alert('Failed to delete todo');
console.error(error);
}
}
},
created() {
this.fetchTodos();
}
}
</script>

<style scoped>
.todo-list {
list-style-type: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.completed .todo-title {
text-decoration: line-through;
color: #888;
}
.delete {
margin-left: auto;
background: none;
border: none;
color: #ff6b6b;
font-size: 18px;
cursor: pointer;
}
.todo-form {
display: flex;
margin-bottom: 20px;
}
.todo-form input {
flex-grow: 1;
padding: 8px;
border: 1px solid #ddd;
}
.loading, .error {
padding: 20px;
text-align: center;
}
.error {
color: #ff6b6b;
}
</style>

Best Practices for REST Requests in Vue.js

  1. Separate API Logic: Keep API calls in separate service files to maintain a clean separation of concerns.

  2. Handle Loading States: Always indicate to users when requests are in progress.

html
<template>
<button @click="submitForm" :disabled="loading">
<span v-if="loading">Submitting...</span>
<span v-else>Submit</span>
</button>
</template>
  1. Error Handling: Implement proper error handling for all API requests.
javascript
async fetchData() {
try {
const response = await ApiService.getData();
this.data = response.data;
} catch (error) {
if (error.response) {
// The server responded with a status code outside the 2xx range
this.handleErrorByStatus(error.response.status);
} else if (error.request) {
// The request was made but no response was received
this.showNetworkError();
} else {
// Something else caused the error
console.error('Error', error.message);
}
}
}
  1. Use Async/Await: Make your code more readable by using async/await for promise handling.

  2. Add Request Timeouts: Prevent requests from hanging indefinitely.

javascript
const apiClient = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000 // 10 seconds
});
  1. Use Interceptors: Centralize common request/response handling.

  2. Cache Responses: Consider caching responses for performance optimization.

javascript
// Simple cache implementation
const cache = new Map();

export default {
async getData(id) {
const cacheKey = `data-${id}`;

if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}

const response = await apiClient.get(`/data/${id}`);
cache.set(cacheKey, response.data);
return response.data;
}
}

Advanced Patterns for REST Requests

Using Composition API (Vue 3)

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

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

const fetchUsers = async () => {
loading.value = true;
error.value = null;

try {
const response = await UserService.getUsers();
users.value = response.data;
} catch (err) {
error.value = err.message;
console.error(err);
} finally {
loading.value = false;
}
};

onMounted(fetchUsers);
</script>

<template>
<div>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</div>
</template>

Creating Reusable Composables

javascript
// composables/useApi.js
import { ref } from 'vue';

export function useApi(apiCall) {
const data = ref(null);
const loading = ref(false);
const error = ref(null);

const execute = async (...args) => {
loading.value = true;
error.value = null;

try {
const response = await apiCall(...args);
data.value = response.data;
return response.data;
} catch (err) {
error.value = err.message || 'An unexpected error occurred';
throw err;
} finally {
loading.value = false;
}
};

return {
data,
loading,
error,
execute
};
}

Using the composable:

html
<script setup>
import { onMounted } from 'vue';
import { useApi } from '@/composables/useApi';
import UserService from '@/services/UserService';

const {
data: users,
loading,
error,
execute: fetchUsers
} = useApi(UserService.getUsers);

onMounted(fetchUsers);
</script>

Summary

In this tutorial, you learned how to perform REST requests in Vue.js applications using both the native Fetch API and Axios. You've seen how to:

  1. Understand REST API basics and HTTP methods
  2. Make GET, POST, PUT, and DELETE requests
  3. Organize API calls using service modules
  4. Handle loading states and errors properly
  5. Implement best practices for REST requests
  6. Use advanced patterns like composables for cleaner code

REST requests are a fundamental part of modern web applications, allowing your Vue.js frontend to communicate with backend services. By following the patterns and practices outlined in this tutorial, you can create maintainable, efficient, and user-friendly applications that handle data loading and submission effectively.

Additional Resources and Exercises

Resources

Exercises

  1. Basic REST Client: Build a simple Vue application that fetches and displays data from a public API like JSONPlaceholder or The Rick and Morty API.

  2. CRUD Application: Create a full CRUD (Create, Read, Update, Delete) application for managing a resource of your choice, like a contact list or book collection.

  3. Error Handling: Improve the error handling in the Todo application we built. Add specific error messages for different error types and implement retry functionality.

  4. Authentication: Extend your REST client to include authentication with JWT tokens and protected routes.

  5. Pagination: Implement pagination for a list of items fetched from an API, showing a limited number of items per page with next/previous navigation.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)