← All guides

Mock a REST API for your Vue app in 60 seconds

The backend isn't ready. It never is. You could hard-code an array of { name: "Test User 1" } objects into your component β€” or you could build against a real HTTP API with realistic data, working pagination and honest loading states, so that when the actual backend lands, the swap is a one-line env change.

1. Get an API (10 seconds)

Click "try it" on the Mockbird landing page, or from a terminal:

curl -X POST https://HOST/api/projects \
  -H 'content-type: application/json' -d '{"name":"vue-demo"}'
# β†’ {"id":"abc123","adminKey":"...","baseUrl":"https://HOST/m/abc123"}

curl -X POST https://HOST/api/projects/abc123/resources \
  -H 'content-type: application/json' -H 'x-admin-key: YOUR_KEY' \
  -d '{"name":"users","template":"users","seed":50}'

You now have 50 realistic users β€” names, emails, avatars, cities β€” at https://HOST/m/abc123/users. CORS is on by default, so the browser can call it from any origin, including localhost.

2. Point Vue at it

// .env.development
VITE_API_URL=https://HOST/m/abc123

// .env.production  (later, when the real backend exists)
VITE_API_URL=https://api.yourapp.com

Then write one composable you'll reuse everywhere β€” reactive inputs in, reactive state out:

// composables/useUsers.js
import { ref, watchEffect } from "vue";

const API = import.meta.env.VITE_API_URL;

export function useUsers(page, search) {
  const users = ref([]);
  const total = ref(0);
  const loading = ref(true);
  const error = ref(null);

  watchEffect((onCleanup) => {
    const ctrl = new AbortController();
    onCleanup(() => ctrl.abort());
    loading.value = true;

    const q = new URLSearchParams({
      page: page.value, limit: 10,
      ...(search.value && { search: search.value }),
    });
    fetch(`${API}/users?${q}`, { signal: ctrl.signal })
      .then((r) => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        total.value = +r.headers.get("X-Total-Count");
        return r.json();
      })
      .then((data) => { users.value = data; error.value = null; loading.value = false; })
      .catch((e) => {
        if (e.name !== "AbortError") { error.value = e.message; loading.value = false; }
      });
  });

  return { users, total, loading, error };
}
<!-- UsersList.vue -->
<script setup>
import { ref } from "vue";
import { useUsers } from "@/composables/useUsers";

const page = ref(1);
const search = ref("");
const { users, total, loading, error } = useUsers(page, search);
</script>

<template>
  <input v-model="search" placeholder="Search users…" />
  <p v-if="loading">Loading…</p>
  <p v-else-if="error">Something broke: {{ error }}</p>
  <ul v-else>
    <li v-for="u in users" :key="u.id">{{ u.firstName }} {{ u.lastName }} β€” {{ u.email }}</li>
  </ul>
  <button :disabled="page === 1" @click="page--">Prev</button>
  <button :disabled="page * 10 >= total" @click="page++">Next</button>
</template>

Because page and search are refs read inside watchEffect, typing in the search box or clicking Next re-fetches automatically, and the AbortController cleanup cancels the stale request β€” no race conditions where an old page's response overwrites a new one. Pagination is real (page/limit, total in the X-Total-Count header), search is real (?search=ana matches across fields), and sorting works too (?sortBy=lastName&order=asc).

3. Test the states you always forget

This is the part hard-coded arrays can never give you. Append a flag to any request:

fetch(`${API}/users?mock_delay=2000`)   // does your skeleton loader actually show?
fetch(`${API}/users?mock_status=500`)   // does your error state render?
fetch(`${API}/users?mock_status=401`)   // does your router guard redirect to login?

No code changes, no browser dev-tools throttling, no conditional mocks β€” just a query param you can also hand to QA or use in Playwright/Cypress specs.

4. Writes work too

Full CRUD is live: POST a new user and it gets the next id; PATCH merges fields; DELETE removes the row. The data persists between reloads, so optimistic updates in your Pinia store get tested against a server that actually changes state. Broke the data while experimenting? Re-seed from the dashboard in one click.

5. Ship day: swap the URL

Because Mockbird follows plain REST conventions, moving to the real backend is changing VITE_API_URL. If your backend team wants a contract to build against, hand them your mock's live OpenAPI spec: https://HOST/m/abc123/openapi.json β€” importable into Swagger UI, Postman, or a code generator.

The same flow works for Nuxt (useFetch against the same URL), React, Svelte, or plain fetch β€” nothing here is Vue-specific except the composable. Free tier: 20 projects, 10k requests/project/day. Docs β†’