Use Axios with Vue.js to Fetch Placeholder Users API

By Maulik Paghdal

02 Dec, 2024

•  8 minutes to Read

Use Axios with Vue.js to Fetch Placeholder Users API

Introduction

Modern web applications demand seamless data fetching and dynamic user interfaces. Vue.js provides an excellent foundation for building reactive components, while Axios offers a robust HTTP client that handles API interactions with minimal configuration. This guide demonstrates how to integrate these technologies to create a responsive user management interface that fetches data from a RESTful API.

We'll build a complete application that retrieves user data from JSONPlaceholder, a popular testing API, and displays it in a clean, responsive layout. Along the way, we'll explore error handling strategies, loading states, and component architecture best practices.

Step 1: Project Setup and Structure

Setting up a Vue.js project correctly from the start saves significant time later. Both Vue CLI and Vite offer excellent scaffolding, but Vite provides faster development builds and hot module replacement.

# Create a Vue project with Vue CLI
vue create vue-axios-example

# Or with Vite (recommended for faster development)
npm create vite@latest vue-axios-example --template vue
cd vue-axios-example
npm install

💡 Tip: Vite's development server typically starts faster and provides better hot reload performance compared to Vue CLI's webpack-based setup.

Project Structure Considerations

After initialization, your project structure should look like this:

vue-axios-example/
├── src/
   ├── components/
   ├── assets/
   ├── App.vue
   └── main.js
├── public/
└── package.json

This structure separates concerns effectively, keeping components modular and assets organized.

Step 2: Installing and Configuring Axios

Axios stands out among HTTP clients for its interceptor support, automatic JSON parsing, and comprehensive error handling capabilities.

npm install axios

Creating an Axios Instance

Rather than importing Axios directly in each component, create a centralized API service. This approach provides better maintainability and allows for global configuration:

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

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

// Request interceptor
api.interceptors.request.use(
  config => {
    console.log('Making request to:', config.url)
    return config
  },
  error => Promise.reject(error)
)

// Response interceptor
api.interceptors.response.use(
  response => response,
  error => {
    console.error('API Error:', error.response?.data || error.message)
    return Promise.reject(error)
  }
)

export default api

⚠️ Warning: Always set reasonable timeout values. The default Axios timeout is 0 (no timeout), which can lead to hanging requests in production.

Step 3: Building the Users Component

The Users component demonstrates several Vue.js patterns: reactive data, lifecycle hooks, and conditional rendering. Here's the enhanced implementation:

<template>
  <div class="container mt-5">
    <div class="d-flex justify-content-between align-items-center mb-4">
      <h1 class="text-primary">User Directory</h1>
      <button 
        @click="refreshUsers" 
        :disabled="loading"
        class="btn btn-outline-primary"
      >
        <span v-if="loading" class="spinner-border spinner-border-sm me-2"></span>
        {{ loading ? 'Loading...' : 'Refresh' }}
      </button>
    </div>

    <!-- Error State -->
    <div v-if="error" class="alert alert-danger" role="alert">
      <h4 class="alert-heading">Error Loading Users</h4>
      <p>{{ error }}</p>
      <button @click="fetchUsers" class="btn btn-danger">Try Again</button>
    </div>

    <!-- Loading State -->
    <div v-else-if="loading" class="text-center py-5">
      <div class="spinner-border text-primary" role="status">
        <span class="visually-hidden">Loading...</span>
      </div>
      <p class="mt-3 text-muted">Fetching user data...</p>
    </div>

    <!-- Users Grid -->
    <div v-else-if="users.length" class="row">
      <div
        v-for="user in users"
        :key="user.id"
        class="col-lg-4 col-md-6 mb-4"
      >
        <div class="card h-100 shadow-sm hover-card">
          <div class="card-body d-flex flex-column">
            <div class="d-flex align-items-center mb-3">
              <div class="avatar-placeholder me-3">
                {{ user.name.charAt(0).toUpperCase() }}
              </div>
              <div>
                <h5 class="card-title mb-1">{{ user.name }}</h5>
                <small class="text-muted">@{{ user.username }}</small>
              </div>
            </div>
            
            <div class="user-details flex-grow-1">
              <p class="card-text">
                <i class="bi bi-envelope me-2"></i>
                <a :href="`mailto:${user.email}`" class="text-decoration-none">
                  {{ user.email }}
                </a>
              </p>
              <p class="card-text">
                <i class="bi bi-telephone me-2"></i>
                <a :href="`tel:${user.phone}`" class="text-decoration-none">
                  {{ formatPhone(user.phone) }}
                </a>
              </p>
              <p class="card-text">
                <i class="bi bi-globe me-2"></i>
                <a :href="`https://${user.website}`" target="_blank" class="text-decoration-none">
                  {{ user.website }}
                </a>
              </p>
              <p class="card-text">
                <i class="bi bi-building me-2"></i>
                {{ user.company.name }}
              </p>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- Empty State -->
    <div v-else class="text-center py-5">
      <i class="bi bi-people display-1 text-muted"></i>
      <h3 class="mt-3">No users found</h3>
      <p class="text-muted">There are no users to display at the moment.</p>
    </div>
  </div>
</template>

<script>
import api from '../services/api'

export default {
  name: "Users",
  data() {
    return {
      users: [],
      loading: false,
      error: null
    }
  },
  methods: {
    async fetchUsers() {
      this.loading = true
      this.error = null
      
      try {
        const response = await api.get('/users')
        this.users = response.data
      } catch (error) {
        this.handleError(error)
      } finally {
        this.loading = false
      }
    },

    async refreshUsers() {
      await this.fetchUsers()
    },

    handleError(error) {
      if (error.code === 'ECONNABORTED') {
        this.error = 'Request timed out. Please check your connection and try again.'
      } else if (error.response?.status >= 500) {
        this.error = 'Server error. Please try again later.'
      } else if (error.response?.status === 404) {
        this.error = 'Users endpoint not found.'
      } else {
        this.error = error.message || 'An unexpected error occurred.'
      }
    },

    formatPhone(phone) {
      // Remove extensions and format basic phone numbers
      return phone.split(' x')[0]
    }
  },

  async mounted() {
    await this.fetchUsers()
  }
}
</script>

<style scoped>
.hover-card {
  transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}

.hover-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 8px 25px rgba(0,0,0,0.15) !important;
}

.avatar-placeholder {
  width: 50px;
  height: 50px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: bold;
  font-size: 1.2rem;
}

.user-details p {
  margin-bottom: 0.5rem;
}

.user-details i {
  width: 16px;
  opacity: 0.7;
}
</style>

Component Architecture Decisions

This implementation showcases several important patterns:

State Management: The component manages three distinct states (loading, error, success) with clear visual feedback for each.

Error Handling: Different error types receive specific handling, providing users with actionable feedback rather than generic error messages.

Accessibility: Proper ARIA roles, semantic HTML, and keyboard navigation support ensure the component works for all users.

📌 Note: The finally block ensures the loading state is cleared regardless of success or failure, preventing stuck loading indicators.

Step 4: Styling and User Experience

Bootstrap 5 provides a solid foundation, but custom CSS enhances the user experience significantly.

npm install bootstrap bootstrap-icons

Update your main.js to include Bootstrap:

import { createApp } from 'vue'
import App from './App.vue'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap-icons/font/bootstrap-icons.css'
import 'bootstrap/dist/js/bootstrap.bundle.min.js'

createApp(App).mount('#app')

Responsive Design Considerations

The card grid uses Bootstrap's responsive classes (col-lg-4 col-md-6) to ensure optimal display across devices:

Screen SizeCards per RowReasoning
Large (≥992px)3Optimal information density
Medium (≥768px)2Maintains readability on tablets
Small (<768px)1Full-width prevents cramping

⚠️ Warning: Always test responsive layouts on actual devices, not just browser dev tools. Physical device constraints often reveal issues desktop testing misses.

Step 5: API Integration Best Practices

Understanding JSONPlaceholder

JSONPlaceholder provides realistic test data without requiring authentication. The users endpoint returns objects with this structure:

{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net"
  },
  "address": {
    "street": "Kulas Light",
    "city": "Gwenborough",
    "zipcode": "92998-3874"
  }
}

Error Handling Strategies

Robust error handling prevents poor user experiences:

// In your component methods
async fetchUsers() {
  try {
    const response = await api.get('/users')
    
    // Validate response structure
    if (!Array.isArray(response.data)) {
      throw new Error('Invalid response format')
    }
    
    this.users = response.data
  } catch (error) {
    this.handleError(error)
  }
}

💡 Tip: Always validate API responses. External APIs can change their data structure without notice, breaking your application.

Step 6: Application Assembly and Deployment

The root App.vue component should remain minimal, focusing on layout and routing concerns:

<template>
  <div id="app">
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
      <div class="container">
        <a class="navbar-brand" href="#">
          <i class="bi bi-people-fill me-2"></i>
          User Management
        </a>
      </div>
    </nav>
    
    <main>
      <Users />
    </main>
    
    <footer class="bg-light py-4 mt-5">
      <div class="container text-center">
        <small class="text-muted">
          Built with Vue.js and Axios • Data from JSONPlaceholder
        </small>
      </div>
    </footer>
  </div>
</template>

<script>
import Users from "./components/Users.vue"

export default {
  name: "App",
  components: {
    Users
  }
}
</script>

<style>
#app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

main {
  flex: 1;
}
</style>

Development and Production Commands

# Development server
npm run dev

# Production build
npm run build

# Preview production build locally
npm run preview

The development server typically runs on http://localhost:5173 for Vite projects.

Advanced Enhancements

Adding Search and Filtering

Extend the component with real-time search capabilities:

// Add to data()
searchQuery: '',
filteredUsers() {
  if (!this.searchQuery) return this.users
  
  return this.users.filter(user => 
    user.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
    user.email.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
    user.company.name.toLowerCase().includes(this.searchQuery.toLowerCase())
  )
}

Performance Optimizations

For larger datasets, consider implementing:

  • Virtual scrolling for hundreds of users
  • Pagination to reduce initial load time
  • Debounced search to prevent excessive API calls
  • Image lazy loading if user avatars are added

Testing Considerations

Key areas to test:

  1. API failure scenarios - Network errors, timeouts, invalid responses
  2. Loading states - Ensure UI remains responsive during data fetching
  3. Responsive behavior - Test across different screen sizes
  4. Accessibility - Screen reader compatibility, keyboard navigation

Conclusion

This implementation demonstrates production-ready patterns for integrating Vue.js with Axios. The component handles multiple states gracefully, provides clear user feedback, and maintains clean separation of concerns. The architecture scales well for more complex applications while remaining maintainable.

Key takeaways include the importance of proper error handling, responsive design considerations, and the benefits of centralized API configuration. These patterns form the foundation for building robust, user-friendly web applications.

Consider extending this example with features like user detail modals, inline editing capabilities, or integration with a state management solution like Pinia for more complex applications.

Topics Covered

About Author

I'm Maulik Paghdal, the founder of Script Binary and a passionate full-stack web developer. I have a strong foundation in both frontend and backend development, specializing in building dynamic, responsive web applications using Laravel, Vue.js, and React.js. With expertise in Tailwind CSS and Bootstrap, I focus on creating clean, efficient, and scalable solutions that enhance user experiences and optimize performance.