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.
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.
// .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).
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.
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.
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.
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 β