The backend isn't ready. It never is. You could hard-code an array of {name: "Test User 1"} objects โ 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":"react-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
const API = import.meta.env.VITE_API_URL;
function useUsers(page, search) {
const [state, setState] = useState({ users: [], total: 0, loading: true, error: null });
useEffect(() => {
const ctrl = new AbortController();
setState(s => ({ ...s, loading: true }));
const q = new URLSearchParams({ page, limit: 10, ...(search && { search }) });
fetch(`${API}/users?${q}`, { signal: ctrl.signal })
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json().then(users => ({ users, total: +r.headers.get("X-Total-Count") }));
})
.then(({ users, total }) => setState({ users, total, loading: false, error: null }))
.catch(e => e.name !== "AbortError" &&
setState(s => ({ ...s, loading: false, error: e.message })));
return () => ctrl.abort();
}, [page, search]);
return state;
}
Pagination is real (page/limit, total count in the X-Total-Count header), search is real (?search=ana matches across fields), and sorting works too (?sortBy=lastName&order=asc). Your UI logic is exercised for real, not against a stub that always succeeds instantly.
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 boundary catch it?
fetch(`${API}/users?mock_status=401`) // does your app redirect to login?
No code changes, no browser dev-tools throttling, no conditional mocks โ just a query param you can also hand to QA.
Full CRUD is live: POST a new user and it gets the next id; PATCH merges fields; DELETE removes the row. The data persists, so your optimistic-update logic gets tested against a server that actually changes state between renders. 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.