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 Size | Cards per Row | Reasoning |
---|---|---|
Large (≥992px) | 3 | Optimal information density |
Medium (≥768px) | 2 | Maintains readability on tablets |
Small (<768px) | 1 | Full-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:
- API failure scenarios - Network errors, timeouts, invalid responses
- Loading states - Ensure UI remains responsive during data fetching
- Responsive behavior - Test across different screen sizes
- 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.