Here's a situation every frontend developer has experienced: the design is done, the component architecture is planned, and you're ready to build — but the backend team is two sprints behind. The real API doesn't exist yet.
You have options. Some are good. Some create debt you'll carry for months.
The Options (and Their Tradeoffs)
Option 1: Hardcode data in the component
// 🔴 Don't do this
const data = { id: 1, name: "Hardcoded Jane", role: "admin" };
This works for initial prototyping but creates tight coupling between your component and fake data. You'll forget to remove it. It creates merge conflicts. It doesn't test loading states.
Option 2: Local JSON files
// 🟡 Acceptable for static data
import mockData from "./mock-user.json";
Better — at least the data is separate from the component. But it doesn't test HTTP behavior: status codes, headers, network latency, error responses, or CORS.
Option 3: A local mock server (MSW, json-server) Great for team projects where everyone runs the same setup. But it requires tooling setup, onboarding, and doesn't help when you want to share a prototype URL with a designer or stakeholder.
Option 4: A real mock API endpoint A live URL that returns your JSON payload with the right status code and CORS headers. Shareable, consistent, and tests real HTTP behavior.
Building Against a Mock Endpoint
Let's walk through a concrete example: building a user profile card that fetches from an API.
First, create your mock payload:
{
"id": "usr_01HXYZ",
"name": "Jane Doe",
"email": "jane@acme.com",
"role": "admin",
"avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=jane",
"joinedAt": "2024-03-15T00:00:00Z",
"lastActive": "2026-02-18T09:30:00Z",
"stats": {
"projectsOwned": 12,
"commitsThisMonth": 47
}
}
Generate a mock endpoint — you'll get a URL like /api/mock/rwstcz3a. Now build your component against it:
interface UserProfile {
id: string;
name: string;
email: string;
role: string;
avatar: string;
joinedAt: string;
stats: { projectsOwned: number; commitsThisMonth: number };
}
function UserProfileCard({ userId }: { userId: string }) {
const [profile, setProfile] = useState<UserProfile | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// In development: swap this URL for the mock endpoint
// In production: use the real API URL
const apiUrl = import.meta.env.DEV
? "https://yourdomain.com/api/mock/rwstcz3a"
: `/api/users/${userId}`;
fetch(apiUrl)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<UserProfile>;
})
.then(setProfile)
.catch((e) => setError(e.message))
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <ProfileSkeleton />;
if (error) return <ErrorState message={error} />;
if (!profile) return null;
return (
<div className="profile-card">
<img src={profile.avatar} alt={profile.name} />
<h2>{profile.name}</h2>
<p>{profile.email}</p>
<Badge>{profile.role}</Badge>
<Stat label="Projects" value={profile.stats.projectsOwned} />
<Stat label="Commits" value={profile.stats.commitsThisMonth} />
</div>
);
}
The key pattern: the component doesn't know or care whether it's talking to a mock or a real API. The URL is injected via environment variable. When the real backend ships, you update one line.
Testing Every State
This is where mock endpoints shine over hardcoded data: you can test all your UI states independently.
Loading State
Introduce artificial delay using the browser's DevTools Network throttling (set to "Slow 3G"). Your component should show a skeleton while fetching.
Success State (200)
The mock endpoint with your normal payload.
Empty State (200 with empty data)
Create a second mock: { "data": [], "total": 0 }. Test that your "No results found" UI renders correctly.
Error States
Create mocks with different error payloads and status codes:
// 404 mock
{ "error": "USER_NOT_FOUND", "message": "No user found with that ID" }
// 403 mock
{ "error": "INSUFFICIENT_PERMISSIONS", "message": "Admin role required" }
// 500 mock
{ "error": "INTERNAL_ERROR", "message": "Something went wrong" }
Using a 404 status mock lets you test that your component handles res.ok === false correctly without writing a single line of backend code.
React Query Pattern
If you're using TanStack Query (React Query), the pattern is the same — just swap the URL:
const MOCK_BASE = "https://yourdomain.com/api/mock";
function useUserProfile(userId: string) {
return useQuery({
queryKey: ["user", userId],
queryFn: async () => {
const url = import.meta.env.DEV
? `${MOCK_BASE}/rwstcz3a`
: `/api/users/${userId}`;
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<UserProfile>;
},
});
}
React Query handles the loading/error/success state transitions, caching, and refetch logic. You get all of that behavior tested against your mock before a real backend exists.
Mocking Paginated Responses
For grids and tables, you'll need paginated mock data:
{
"page": 1,
"perPage": 20,
"total": 142,
"data": [
{ "id": 1, "name": "Alpha", "status": "active" },
{ "id": 2, "name": "Beta", "status": "inactive" },
{ "id": 3, "name": "Gamma", "status": "active" }
]
}
Create separate mocks for page 1, page 2, and the last page. Wire your pagination component to switch between them using a URL parameter.
Transitioning to the Real API
When the backend ships, the transition should be a config change, not a refactor:
- Replace the mock URL with the real API URL in your environment config
- Verify the real API response shape matches your TypeScript interface
- Remove the
import.meta.env.DEVbranch (or keep it for offline development)
If the real API returns a different shape than your mock, you'll know immediately — TypeScript will error and your UI will likely break. This is good: you caught the mismatch at integration time, not in production.
What Mock Endpoints Can't Replace
Mock endpoints are a development accelerator, not a complete testing strategy. They don't replace:
- Contract testing (ensuring real API shape matches TypeScript types)
- Integration tests (component + real backend)
- Performance testing (real network conditions, real database load)
- Authentication testing (real token flows)
Use mocks to move fast in isolation. Use a staging environment with real APIs for integration testing. The workflow isn't "either/or" — it's "mock first, integrate when ready."
The habit of building against mock endpoints before the backend is ready is one of the highest-leverage habits a frontend developer can build. It forces you to think about all your UI states upfront and eliminates the "waiting for the backend team" bottleneck entirely.