Vue Tutorial · Lessons

Foundation

Basic

Medium

Advanced

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.

Put 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.

Libraries such as VueUse (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/json so Laravel returns JSON errors, and check res.ok, because fetch doesn'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, and AbortController to cancel stale requests.
  • Build query strings with URLSearchParams, and send JSON with method, Content-Type and JSON.stringify.
  • Extract the pattern into a useFetch composable once you need it twice.