API Integration

Learn how to integrate Vue.js applications with external APIs. This guide covers making HTTP requests, handling responses, and managing API data effectively.

Setting Up API Integration

Installing Axios

Axios is the most popular HTTP client for JavaScript applications:

npm install axios

Creating an API Service

Organize your API calls in a dedicated service file:

// services/api.js
import axios from 'axios'

// Create axios instance with base configuration
const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// Request interceptor for adding auth token
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('authToken')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// Response interceptor for handling errors
api.interceptors.response.use(
  (response) => {
    return response.data
  },
  (error) => {
    if (error.response?.status === 401) {
      // Handle unauthorized access
      localStorage.removeItem('authToken')
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

export default api

Making API Calls in Components

Basic GET Request

<!-- components/UserList.vue -->
<template>
  <div class="user-list">
    <h2>Users</h2>
    
    <div v-if="loading" class="loading">
      Loading users...
    </div>
    
    <div v-else-if="error" class="error">
      {{ error }}
    </div>
    
    <div v-else class="users">
      <div v-for="user in users" :key="user.id" class="user-card">
        <h3>{{ user.name }}</h3>
        <p>{{ user.email }}</p>
        <p>Role: {{ user.role }}</p>
      </div>
    </div>
    
    <button @click="fetchUsers" :disabled="loading">
      {{ loading ? 'Loading...' : 'Refresh Users' }}
    </button>
  </div>
</template>

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

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

const fetchUsers = async () => {
  loading.value = true
  error.value = null
  
  try {
    const response = await api.get('/users')
    users.value = response.data
  } catch (err) {
    error.value = 'Failed to fetch users. Please try again.'
    console.error('Error fetching users:', err)
  } finally {
    loading.value = false
  }
}

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

<style scoped>
.user-list {
  padding: 20px;
}

.loading, .error {
  text-align: center;
  padding: 20px;
  color: #666;
}

.error {
  color: #dc3545;
}

.users {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 20px;
  margin-bottom: 20px;
}

.user-card {
  padding: 15px;
  border: 1px solid #ddd;
  border-radius: 8px;
  background-color: #f9f9f9;
}

.user-card h3 {
  margin: 0 0 10px 0;
  color: #42b883;
}

button {
  padding: 10px 20px;
  background-color: #42b883;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}
</style>

POST Request with Form Data

<!-- components/CreateUser.vue -->
<template>
  <div class="create-user">
    <h2>Create New User</h2>
    
    <form @submit.prevent="createUser" class="user-form">
      <div class="form-group">
        <label for="name">Name:</label>
        <input 
          id="name"
          v-model="formData.name" 
          type="text" 
          required 
        />
      </div>
      
      <div class="form-group">
        <label for="email">Email:</label>
        <input 
          id="email"
          v-model="formData.email" 
          type="email" 
          required 
        />
      </div>
      
      <div class="form-group">
        <label for="role">Role:</label>
        <select id="role" v-model="formData.role" required>
          <option value="">Select a role</option>
          <option value="user">User</option>
          <option value="admin">Admin</option>
          <option value="moderator">Moderator</option>
        </select>
      </div>
      
      <button type="submit" :disabled="submitting">
        {{ submitting ? 'Creating...' : 'Create User' }}
      </button>
      
      <div v-if="success" class="success">
        User created successfully!
      </div>
      
      <div v-if="error" class="error">
        {{ error }}
      </div>
    </form>
  </div>
</template>

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

const formData = ref({
  name: '',
  email: '',
  role: ''
})

const submitting = ref(false)
const success = ref(false)
const error = ref(null)

const createUser = async () => {
  submitting.value = true
  success.value = false
  error.value = null
  
  try {
    const response = await api.post('/users', formData.value)
    success.value = true
    
    // Reset form
    formData.value = {
      name: '',
      email: '',
      role: ''
    }
    
    // Emit event to parent component
    emit('user-created', response.data)
  } catch (err) {
    error.value = 'Failed to create user. Please try again.'
    console.error('Error creating user:', err)
  } finally {
    submitting.value = false
  }
}

const emit = defineEmits(['user-created'])
</script>

<style scoped>
.create-user {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
}

.user-form {
  display: flex;
  flex-direction: column;
  gap: 15px;
}

.form-group {
  display: flex;
  flex-direction: column;
}

label {
  margin-bottom: 5px;
  font-weight: bold;
}

input, select {
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

button {
  padding: 10px 20px;
  background-color: #42b883;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

.success {
  margin-top: 10px;
  padding: 10px;
  background-color: #d4edda;
  color: #155724;
  border-radius: 4px;
  text-align: center;
}

.error {
  margin-top: 10px;
  padding: 10px;
  background-color: #f8d7da;
  color: #721c24;
  border-radius: 4px;
  text-align: center;
}
</style>

Advanced API Patterns

Composable for API Calls

Create reusable composables for common API operations:

// composables/useApi.js
import { ref } from 'vue'
import api from '@/services/api'

export function useApi() {
  const loading = ref(false)
  const error = ref(null)
  
  const execute = async (apiCall) => {
    loading.value = true
    error.value = null
    
    try {
      const result = await apiCall()
      return result
    } catch (err) {
      error.value = err.message || 'An error occurred'
      throw err
    } finally {
      loading.value = false
    }
  }
  
  return {
    loading,
    error,
    execute
  }
}

// Specific composable for users
export function useUsers() {
  const { loading, error, execute } = useApi()
  const users = ref([])
  
  const fetchUsers = async () => {
    const result = await execute(() => api.get('/users'))
    users.value = result.data
    return result
  }
  
  const createUser = async (userData) => {
    const result = await execute(() => api.post('/users', userData))
    return result
  }
  
  const updateUser = async (id, userData) => {
    const result = await execute(() => api.put(`/users/${id}`, userData))
    return result
  }
  
  const deleteUser = async (id) => {
    const result = await execute(() => api.delete(`/users/${id}`))
    return result
  }
  
  return {
    users,
    loading,
    error,
    fetchUsers,
    createUser,
    updateUser,
    deleteUser
  }
}

Using the Composable

<!-- components/UserManager.vue -->
<template>
  <div class="user-manager">
    <h2>User Manager</h2>
    
    <button @click="loadUsers" :disabled="loading">
      {{ loading ? 'Loading...' : 'Load Users' }}
    </button>
    
    <div v-if="error" class="error">
      {{ error }}
    </div>
    
    <div class="users-grid">
      <div v-for="user in users" :key="user.id" class="user-item">
        <h3>{{ user.name }}</h3>
        <p>{{ user.email }}</p>
        <button @click="deleteUser(user.id)" class="delete-btn">
          Delete
        </button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { useUsers } from '@/composables/useUsers'

const { users, loading, error, fetchUsers, deleteUser } = useUsers()

const loadUsers = async () => {
  await fetchUsers()
}
</script>

<style scoped>
.user-manager {
  padding: 20px;
}

.users-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 15px;
  margin-top: 20px;
}

.user-item {
  padding: 15px;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.delete-btn {
  background-color: #dc3545;
  color: white;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
}

.error {
  color: #dc3545;
  margin: 10px 0;
}
</style>

Error Handling Best Practices

Global Error Handler

Create a global error handler for API errors:

// utils/errorHandler.js
export const handleApiError = (error) => {
  if (error.response) {
    // Server responded with error status
    switch (error.response.status) {
      case 400:
        return 'Bad request. Please check your input.'
      case 401:
        return 'Unauthorized. Please log in again.'
      case 403:
        return 'Forbidden. You don\'t have permission to do this.'
      case 404:
        return 'Resource not found.'
      case 500:
        return 'Server error. Please try again later.'
      default:
        return `Error: ${error.response.status}`
    }
  } else if (error.request) {
    // Request was made but no response received
    return 'Network error. Please check your connection.'
  } else {
    // Something else happened
    return error.message || 'An unexpected error occurred.'
  }
}

Retry Logic

Implement retry logic for failed requests:

// utils/retry.js
export const retryRequest = async (apiCall, maxRetries = 3, delay = 1000) => {
  let lastError
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await apiCall()
    } catch (error) {
      lastError = error
      
      if (i < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i)))
      }
    }
  }
  
  throw lastError
}

Caching and Performance

Simple Cache Implementation

// utils/cache.js
const cache = new Map()

export const cachedApiCall = async (key, apiCall, ttl = 60000) => {
  const cached = cache.get(key)
  
  if (cached && Date.now() - cached.timestamp < ttl) {
    return cached.data
  }
  
  const data = await apiCall()
  cache.set(key, {
    data,
    timestamp: Date.now()
  })
  
  return data
}

Next Steps

Now that you understand API integration, explore Vue.js testing to ensure your API calls work correctly.

External Resources

Last updated on