Fetching Data from an API
fetch, async/await, loading and error states against a real Laravel API.
Most Vue apps exist to show and change data that lives on a server. This lesson covers the patterns you'll use in almost every one: loading data when a component appears, showing loading and error states, reloading when a filter changes, cancelling stale requests and sending data back.
Every example calls this site's real Laravel API (/api/demo/...). It serves the Projects & Tasks data built in the Laravel tutorial: the routes are in routes/api.php, and the JSON shape comes from API Resources (see Building APIs).
Look at the API first
Before writing any Vue, look at what the endpoint returns. Press Send, or edit the URL and try /api/demo/projects/website-relaunch or /api/demo/tasks?priority=high.
Laravel's resource collections wrap the rows in a data key, so in JavaScript you'll almost always write const { data } = await res.json().
Fetch when the component mounts
The simplest pattern: a ref for the data, and a fetch in onMounted. Send Accept: application/json so Laravel returns JSON errors (a 404 or 422) instead of an HTML page.
Loading, error and empty states
Real requests are slow sometimes and fail sometimes. Every data-driven component needs four states: loading, error, empty and loaded. Note that fetch only rejects on network failure. A 404 or 500 still resolves, so check res.ok yourself.
loading = false in finally, so it runs whether the request succeeded or threw. Forgetting it is the classic "spinner that never stops" bug.
Reloading when something changes
When the data depends on user input, such as a selected project or a filter, watch the input and reload. { immediate: true } also runs it once on start, so you don't need onMounted as well.
URLSearchParams builds the query string and encodes values safely. Never glue user input into a URL by hand.
Cancelling stale requests
If the user changes the filter twice quickly, two requests race. If the first one finishes last, the screen shows the wrong results. AbortController cancels the earlier request, and watch's onCleanup (onWatcherCleanup in Vue 3.5+) is the right place to do it.
watch(priority, async (value, oldValue, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort()) // runs before the next change
try {
const res = await fetch(`/api/demo/tasks?priority=${value}`, { signal: controller.signal })
tasks.value = (await res.json()).data
} catch (e) {
if (e.name !== 'AbortError') error.value = e.message // aborts are expected
}
}, { immediate: true })
The project browser at the end of this lesson uses exactly this. Click between projects quickly and only the last one's tasks appear.
Loading one item by its URL key
Laravel's route model binding looks the project up by its slug (getRouteKeyName() in app/Models/Project.php), so the URL is /api/demo/projects/{slug}. An unknown slug returns a JSON 404, which the error state handles.
Sending data with POST
To send data, set method, a JSON Content-Type and a body. This endpoint validates a new task with Laravel's StoreTaskRequest without saving it. Send it as it is to see the 422 validation errors, then fix the body and send again.
The same request from Vue:
const res = await fetch('/api/demo/tasks/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ project_id: project.id, title: 'Ship it', priority: 'high' }),
})
const body = await res.json()
if (res.status === 422) {
errors.value = body.errors // { title: ['…'], priority: ['…'] }
}
The Form Validation lesson turns those errors into messages under each field.
Reusing it: a useFetch composable
Once you've written the loading/error/data trio twice, extract it into a composable. It takes a URL (or a ref/getter), returns reactive state, and refetches when the URL changes.
useFetch) and TanStack Query add caching, retries and background refetching on top of this pattern. Write it yourself first, so you know what they're doing for you.
Putting it together: a project browser
A list of projects, a details panel with that project's tasks, skeleton loading, error handling and request cancellation, all against the live API. Open your browser's Network tab and click quickly between projects to watch earlier requests get cancelled.
Key takeaways
- Look at the endpoint's JSON first. Laravel resources wrap results in
data. - Send
Accept: application/jsonso Laravel returns JSON errors, and checkres.ok, becausefetchdoesn't reject on 4xx/5xx. - Model four states (loading, error, empty, loaded) and reset loading in
finally. - Use
watch(source, fn, { immediate: true })to load and reload when inputs change, andAbortControllerto cancel stale requests. - Build query strings with
URLSearchParams, and send JSON withmethod,Content-TypeandJSON.stringify. - Extract the pattern into a
useFetchcomposable once you need it twice.