HTTP Requests with fetch and axios

Intermediate
13 min

HTTP Requests with fetch and axios

Vue has no HTTP layer of its own; you use the browser's fetch or a library such as axios and connect the result to reactive state. The hard parts are everything around the request: loading and error states, overlapping requests, authentication headers, and configuration that differs between development and production. After this lesson you will handle all of these cleanly.

Loading, Data and Error with fetch

Every request needs three states. Track them explicitly and render each one:

vue
<script setup> import { ref, onMounted } from 'vue' const products = ref([]) const loading = ref(false) const error = ref(null) async function load() { loading.value = true error.value = null try { const res = await fetch('/api/products') if (!res.ok) throw new Error(`Request failed with status ${res.status}`) products.value = await res.json() } catch (e) { error.value = e.message } finally { loading.value = false } } onMounted(load) </script> <template> <p v-if="loading">Loading...</p> <p v-else-if="error" role="alert">{{ error }}</p> <ul v-else> <li v-for="p in products" :key="p.id">{{ p.name }}</li> </ul> </template>

fetch only rejects on network failure; a 404 or 500 resolves normally, so always check res.ok. Sending data uses the same call with method: 'POST', a 'Content-Type': 'application/json' header and body: JSON.stringify(payload).

Cancelling Stale Requests

When a search box fires a request per keystroke, responses can arrive out of order. Cancel the previous request with an AbortController inside a watcher cleanup:

javascript
import { ref, watch, onWatcherCleanup } from 'vue' const query = ref('') const results = ref([]) watch(query, async (q) => { const controller = new AbortController() onWatcherCleanup(() => controller.abort()) try { const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: controller.signal }) results.value = await res.json() } catch (e) { if (e.name !== 'AbortError') throw e } })

Always encode user input in URLs with encodeURIComponent, and debounce the watcher (for example with VueUse's useDebounceFn) so the request fires only after the user pauses.

Using axios

axios adds conveniences on top of fetch: automatic JSON parsing, rejection on non-2xx statuses, a params option, timeouts, and interceptors. Create one configured instance and import it everywhere:

javascript
// src/api/client.js import axios from 'axios' export const api = axios.create({ baseURL: import.meta.env.VITE_API_URL, timeout: 10000 }) api.interceptors.request.use((config) => { const token = localStorage.getItem('token') if (token) config.headers.Authorization = `Bearer ${token}` return config }) api.interceptors.response.use( (res) => res, (err) => { if (err.response?.status === 401) window.location.assign('/login') return Promise.reject(err) } )

Usage: const { data } = await api.get('/products', { params: { page: 2 } }) or await api.post('/products', payload). Errors carry err.response.status and err.response.data, so components can show the server's message. Cancellation uses the same AbortController via the signal option.

| | fetch | axios | | --- | --- | --- | | Non-2xx handling | Resolves; check res.ok | Rejects automatically | | JSON | await res.json() | Parsed into data | | Interceptors | Write a wrapper | Built in | | Upload progress | Not available | onUploadProgress |

Environment Variables and the Dev Proxy

Vite exposes variables prefixed with VITE_ from .env files as import.meta.env.VITE_*. Put VITE_API_URL in .env.development and .env.production; never put secrets there, because the client bundle is public.

During development, a proxy avoids CORS entirely by forwarding /api to the backend from the same origin:

javascript
// vite.config.js export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } } } })

Organizing API Code

Keep request code out of components: a src/api/ folder with one module per resource (products.js exporting list(), get(id), create(data)), plus composables such as useFetch or a Pinia store for shared data. For caching, deduplication, retries and background refetching, consider TanStack Query (@tanstack/vue-query) with its useQuery and useMutation composables.

Common mistakes

  • Forgetting res.ok with fetch, so HTTP errors are rendered as data.
  • Setting loading = false outside finally, which leaves the spinner spinning after an error.
  • Firing a request once in <script setup> and expecting it to re-run when props change; use a watcher.
Quick Quiz
Question 1 of 2

What does `fetch` do when the server responds with status 500?

Key Takeaways

  • Track loading, error and data explicitly; reset loading in finally.
  • fetch needs a res.ok check; axios rejects non-2xx responses and parses JSON automatically.
  • Cancel stale requests with AbortController in a watcher cleanup and encode user input in URLs.
  • Create one axios instance with baseURL and interceptors for auth and error handling.
  • Use VITE_ environment variables and the dev proxy; keep request code in src/api/ modules or composables.

Next lesson: Error Handling in Vue Applications — capture render, async and routing errors with errorHandler, onErrorCaptured and error boundaries.

HTTP Requests with fetch and axios - Vue.js | CodeYourCraft | CodeYourCraft