API Mock Server
Create realistic mock APIs for testing and development.
Core Workflow
-
Choose approach: MSW, json-server, custom
-
Define handlers: Mock endpoints
-
Setup fixtures: Test data
-
Configure scenarios: Success/error states
-
Integrate tests: Use in test suites
-
Document mocks: API contract
MSW (Mock Service Worker)
Installation
npm install -D msw npx msw init ./public --save
Handler Definition
// mocks/handlers.ts import { http, HttpResponse, delay } from 'msw';
// Types interface User { id: string; name: string; email: string; role: 'admin' | 'user'; }
interface CreateUserInput { name: string; email: string; }
// Fixtures const users: User[] = [ { id: '1', name: 'John Doe', email: 'john@example.com', role: 'admin' }, { id: '2', name: 'Jane Smith', email: 'jane@example.com', role: 'user' }, ];
// Handlers export const handlers = [ // GET /api/users http.get('/api/users', async () => { await delay(100); return HttpResponse.json(users); }),
// GET /api/users/:id http.get('/api/users/:id', async ({ params }) => { await delay(50); const user = users.find((u) => u.id === params.id);
if (!user) {
return new HttpResponse(null, { status: 404 });
}
return HttpResponse.json(user);
}),
// POST /api/users http.post('/api/users', async ({ request }) => { await delay(100); const body = (await request.json()) as CreateUserInput;
const newUser: User = {
id: String(users.length + 1),
name: body.name,
email: body.email,
role: 'user',
};
users.push(newUser);
return HttpResponse.json(newUser, { status: 201 });
}),
// PUT /api/users/:id http.put('/api/users/:id', async ({ params, request }) => { await delay(50); const body = (await request.json()) as Partial<User>; const index = users.findIndex((u) => u.id === params.id);
if (index === -1) {
return new HttpResponse(null, { status: 404 });
}
users[index] = { ...users[index], ...body };
return HttpResponse.json(users[index]);
}),
// DELETE /api/users/:id http.delete('/api/users/:id', async ({ params }) => { await delay(50); const index = users.findIndex((u) => u.id === params.id);
if (index === -1) {
return new HttpResponse(null, { status: 404 });
}
users.splice(index, 1);
return new HttpResponse(null, { status: 204 });
}), ];
Setup for Browser
// mocks/browser.ts import { setupWorker } from 'msw/browser'; import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// main.tsx or app entry async function enableMocking() { if (process.env.NODE_ENV === 'development') { const { worker } = await import('./mocks/browser'); return worker.start({ onUnhandledRequest: 'bypass', }); } }
enableMocking().then(() => { // Render app });
Setup for Node (Tests)
// mocks/server.ts import { setupServer } from 'msw/node'; import { handlers } from './handlers';
export const server = setupServer(...handlers);
// tests/setup.ts (Jest or Vitest) import { beforeAll, afterEach, afterAll } from 'vitest'; import { server } from '../mocks/server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); afterEach(() => server.resetHandlers()); afterAll(() => server.close());
Test-Specific Handlers
// tests/users.test.ts import { http, HttpResponse } from 'msw'; import { server } from '../mocks/server'; import { render, screen, waitFor } from '@testing-library/react'; import { UserList } from '../components/UserList';
describe('UserList', () => { it('displays users', async () => { render(<UserList />);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
it('handles empty state', async () => { server.use( http.get('/api/users', () => { return HttpResponse.json([]); }) );
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('No users found')).toBeInTheDocument();
});
});
it('handles server error', async () => { server.use( http.get('/api/users', () => { return new HttpResponse(null, { status: 500 }); }) );
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('Error loading users')).toBeInTheDocument();
});
});
it('handles network error', async () => { server.use( http.get('/api/users', () => { return HttpResponse.error(); }) );
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('Network error')).toBeInTheDocument();
});
}); });
GraphQL Mocking
// mocks/graphql-handlers.ts import { graphql, HttpResponse } from 'msw';
export const graphqlHandlers = [ graphql.query('GetUsers', () => { return HttpResponse.json({ data: { users: [ { id: '1', name: 'John', email: 'john@example.com' }, { id: '2', name: 'Jane', email: 'jane@example.com' }, ], }, }); }),
graphql.mutation('CreateUser', ({ variables }) => { return HttpResponse.json({ data: { createUser: { id: '3', name: variables.input.name, email: variables.input.email, }, }, }); }),
graphql.query('GetUser', ({ variables }) => { if (variables.id === 'not-found') { return HttpResponse.json({ data: { user: null }, errors: [{ message: 'User not found' }], }); }
return HttpResponse.json({
data: {
user: { id: variables.id, name: 'John', email: 'john@example.com' },
},
});
}), ];
Json-Server
Configuration
// db.json { "users": [ { "id": "1", "name": "John Doe", "email": "john@example.com" }, { "id": "2", "name": "Jane Smith", "email": "jane@example.com" } ], "posts": [ { "id": "1", "title": "Hello World", "authorId": "1" }, { "id": "2", "title": "Another Post", "authorId": "2" } ], "comments": [ { "id": "1", "text": "Great post!", "postId": "1" } ] }
// json-server.config.js module.exports = { port: 3001, watch: true, delay: 100, routes: 'routes.json', };
// routes.json { "/api/*": "/$1", "/users/:id/posts": "/posts?authorId=:id" }
Custom Routes
// server.js const jsonServer = require('json-server'); const server = jsonServer.create(); const router = jsonServer.router('db.json'); const middlewares = jsonServer.defaults();
// Custom middleware for auth server.use((req, res, next) => { if (req.headers.authorization !== 'Bearer valid-token') { if (req.method !== 'GET') { return res.status(401).json({ error: 'Unauthorized' }); } } next(); });
server.use(middlewares);
// Custom routes server.get('/api/me', (req, res) => { res.json({ id: '1', name: 'Current User', email: 'me@example.com' }); });
server.post('/api/login', (req, res) => { const { email, password } = req.body; if (email === 'test@example.com' && password === 'password') { res.json({ token: 'valid-token', user: { id: '1', email } }); } else { res.status(401).json({ error: 'Invalid credentials' }); } });
server.use('/api', router);
server.listen(3001, () => { console.log('Mock API running on http://localhost:3001'); });
Fixture Factories
// mocks/factories/user.ts import { faker } from '@faker-js/faker';
interface User { id: string; name: string; email: string; role: 'admin' | 'user'; createdAt: string; }
export function createUser(overrides: Partial<User> = {}): User { return { id: faker.string.uuid(), name: faker.person.fullName(), email: faker.internet.email(), role: 'user', createdAt: faker.date.past().toISOString(), ...overrides, }; }
export function createUsers(count: number, overrides: Partial<User> = {}): User[] { return Array.from({ length: count }, () => createUser(overrides)); }
// mocks/factories/index.ts export * from './user'; export * from './post'; export * from './comment';
Using Factories in Tests
// tests/components/UserProfile.test.tsx import { createUser } from '../mocks/factories'; import { server } from '../mocks/server'; import { http, HttpResponse } from 'msw';
describe('UserProfile', () => { it('displays admin badge for admin users', async () => { const adminUser = createUser({ role: 'admin', name: 'Admin User' });
server.use(
http.get('/api/users/:id', () => {
return HttpResponse.json(adminUser);
})
);
render(<UserProfile userId={adminUser.id} />);
await waitFor(() => {
expect(screen.getByText('Admin User')).toBeInTheDocument();
expect(screen.getByTestId('admin-badge')).toBeInTheDocument();
});
}); });
Scenario-Based Mocking
// mocks/scenarios.ts import { http, HttpResponse, delay } from 'msw'; import { createUser, createUsers } from './factories';
export const scenarios = { default: [],
emptyState: [ http.get('/api/users', () => HttpResponse.json([])), http.get('/api/posts', () => HttpResponse.json([])), ],
loadingState: [ http.get('/api/users', async () => { await delay('infinite'); return HttpResponse.json([]); }), ],
errorState: [ http.get('/api/users', () => { return new HttpResponse(null, { status: 500 }); }), ],
largeDataset: [ http.get('/api/users', () => { return HttpResponse.json(createUsers(1000)); }), ],
slowNetwork: [ http.get('/api/*', async ({ request }) => { await delay(2000); // Continue to default handler return undefined; }), ], };
// Usage in tests describe('UserList scenarios', () => { it('empty state', async () => { server.use(...scenarios.emptyState); // Test empty state });
it('error state', async () => { server.use(...scenarios.errorState); // Test error handling }); });
// Usage in Storybook export const EmptyState: Story = { parameters: { msw: { handlers: scenarios.emptyState, }, }, };
Playwright Integration
// tests/e2e/api-mocking.spec.ts import { test, expect } from '@playwright/test';
test.describe('API Mocking', () => { test('mock successful response', async ({ page }) => { await page.route('/api/users', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([ { id: '1', name: 'Mocked User', email: 'mock@example.com' }, ]), }); });
await page.goto('/users');
await expect(page.getByText('Mocked User')).toBeVisible();
});
test('mock error response', async ({ page }) => { await page.route('/api/users', async (route) => { await route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ error: 'Server Error' }), }); });
await page.goto('/users');
await expect(page.getByText('Error loading users')).toBeVisible();
});
test('delay response', async ({ page }) => { await page.route('/api/users', async (route) => { await new Promise((resolve) => setTimeout(resolve, 3000)); await route.fulfill({ status: 200, body: JSON.stringify([]), }); });
await page.goto('/users');
await expect(page.getByTestId('loading-spinner')).toBeVisible();
}); });
Best Practices
-
Use factories: Generate realistic data
-
Define scenarios: Reusable mock configurations
-
Test edge cases: Errors, empty states, loading
-
Keep handlers simple: One responsibility each
-
Match production API: Same contracts
-
Delay appropriately: Realistic timing
-
Document mocks: API contracts
-
Reset between tests: Clean state
Output Checklist
Every API mock setup should include:
-
MSW/json-server configuration
-
Handler definitions
-
Test setup integration
-
Fixture factories
-
Error scenarios
-
Loading states
-
GraphQL support (if needed)
-
Browser integration
-
E2E test mocking
-
Documentation