
Msw Mocking
- 22 installs
- 213 repo stars
- Updated August 4, 2026
- yonatangross/orchestkit
Helps with ai & agent building tasks.
About
msw-mocking is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- msw-mocking
- AI & Agent Building
- AI-coding skill
Msw Mocking by the numbers
- 22 all-time installs (skills.sh)
- Ranked #10,169 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yonatangross/orchestkit --skill msw-mockingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 22 |
|---|---|
| repo stars | ★ 213 |
| Last updated | August 4, 2026 |
| Repository | yonatangross/orchestkit ↗ |
What it does
Helps with ai & agent building tasks.
Files
MSW (Mock Service Worker) 2.x
Network-level API mocking for frontend tests using MSW 2.x.
Quick Reference
// Core imports
import { http, HttpResponse, graphql, ws, delay, passthrough } from 'msw';
import { setupServer } from 'msw/node';
// Basic handler
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({ id: params.id, name: 'User' });
});
// Error response
http.get('/api/fail', () => {
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
});
// Delay simulation
http.get('/api/slow', async () => {
await delay(2000);
return HttpResponse.json({ data: 'response' });
});
// Passthrough (NEW in 2.x)
http.get('/api/real', () => passthrough());Test Setup
// vitest.setup.ts
import { beforeAll, afterEach, afterAll } from 'vitest';
import { server } from './src/mocks/server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());Runtime Override
import { http, HttpResponse } from 'msw';
import { server } from '../mocks/server';
test('shows error on API failure', async () => {
server.use(
http.get('/api/users/:id', () => {
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
})
);
render(<UserProfile id="123" />);
expect(await screen.findByText(/not found/i)).toBeInTheDocument();
});Anti-Patterns (FORBIDDEN)
// ❌ NEVER mock fetch directly
jest.spyOn(global, 'fetch').mockResolvedValue(...)
// ❌ NEVER mock axios module
jest.mock('axios')
// ❌ NEVER test implementation details
expect(fetch).toHaveBeenCalledWith('/api/...')
// ✅ ALWAYS use MSW
server.use(http.get('/api/...', () => HttpResponse.json({...})))
// ✅ ALWAYS test user-visible behavior
expect(await screen.findByText('Success')).toBeInTheDocument()Key Decisions
| Decision | Recommendation |
|---|---|
| Handler location | src/mocks/handlers.ts |
| Default behavior | Return success |
| Override scope | Per-test with server.use() |
| Unhandled requests | Error (catch missing mocks) |
| GraphQL | Use graphql.query/mutation |
| WebSocket | Use ws.link() for WS mocking |
Detailed Documentation
| Resource | Description |
|---|---|
| references/msw-2x-api.md | Complete MSW 2.x API reference |
| examples/handler-patterns.md | CRUD, auth, error, and upload examples |
| checklists/msw-setup-checklist.md | Setup and review checklists |
| scripts/handlers-template.ts | Starter template for new handlers |
Related Skills
unit-testing- Component isolationintegration-testing- Full integration testsvcr-http-recording- Python equivalent
Capability Details
http-request-mocking
Keywords: http.get, http.post, http handler, REST mock Solves:
- Mock REST API endpoints
- Intercept HTTP requests at network level
- Create request handlers for testing
graphql-mocking
Keywords: graphql.query, graphql.mutation, GraphQL handler, mock GraphQL Solves:
- Mock GraphQL queries and mutations
- Handle GraphQL variables in mocks
- Test GraphQL error scenarios
websocket-mocking
Keywords: WebSocket, ws mock, real-time mock, socket mock Solves:
- Mock WebSocket connections
- Simulate real-time events
- Test WebSocket message handling
error-simulation
Keywords: error simulation, network error, 500 error, mock error Solves:
- Simulate API errors in tests
- Test error handling UI
- Mock network failures
network-delay-simulation
Keywords: delay, latency, slow response, loading state Solves:
- Simulate slow network responses
- Test loading state UI
- Verify timeout handling
runtime-handler-override
Keywords: runtime override, use.once, test-specific handler, override Solves:
- Override handlers for specific tests
- Create one-time response handlers
- Customize responses per test
MSW Setup Checklist
Initial Setup
- [ ] Install MSW 2.x:
npm install msw@latest --save-dev - [ ] Initialize MSW:
npx msw init ./public --save - [ ] Create
src/mocks/directory structure
Directory Structure
src/mocks/
├── handlers/
│ ├── index.ts # Export all handlers
│ ├── users.ts # User-related handlers
│ ├── auth.ts # Auth handlers
│ └── ...
├── handlers.ts # Combined handlers
├── server.ts # Node.js server (tests)
└── browser.ts # Browser worker (dev/storybook)Test Configuration (Vitest)
- [ ] Create
src/mocks/server.ts:
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);- [ ] Update
vitest.setup.ts:
import { beforeAll, afterEach, afterAll } from 'vitest';
import { server } from './src/mocks/server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());- [ ] Update
vitest.config.ts:
export default defineConfig({
test: {
setupFiles: ['./vitest.setup.ts'],
},
});Handler Implementation Checklist
For each API endpoint:
- [ ] Implement success response with realistic data
- [ ] Handle path parameters (
/:id) - [ ] Handle query parameters (pagination, filters)
- [ ] Handle request body for POST/PUT/PATCH
- [ ] Implement error responses (400, 401, 403, 404, 422, 500)
- [ ] Add authentication checks where applicable
- [ ] Export handler from
handlers/index.ts
Test Writing Checklist
For each component:
- [ ] Test happy path (success response)
- [ ] Test loading state
- [ ] Test error state (API failure)
- [ ] Test empty state (no data)
- [ ] Test validation errors
- [ ] Test authentication errors
- [ ] Use
server.use()for test-specific overrides - [ ] Cleanup:
server.resetHandlers()runs in afterEach
Common Issues Checklist
- [ ] Verify
onUnhandledRequest: 'error'catches missing handlers - [ ] Check handler URL patterns match actual API calls
- [ ] Ensure async handlers use
await request.json() - [ ] Verify response status codes are correct
- [ ] Check Content-Type headers for non-JSON responses
Storybook Integration (Optional)
- [ ] Create
src/mocks/browser.ts:
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);- [ ] Initialize in
.storybook/preview.ts:
import { initialize, mswLoader } from 'msw-storybook-addon';
initialize();
export const loaders = [mswLoader];- [ ] Add
msw-storybook-addonto dependencies
Review Checklist
Before PR:
- [ ] All handlers return realistic mock data
- [ ] Error scenarios are covered
- [ ] No hardcoded tokens/secrets in handlers
- [ ] Handlers are organized by domain (users, auth, etc.)
- [ ] Tests use
server.use()for overrides, not new handlers - [ ] Loading states tested with
delay()
MSW Handler Patterns
Complete Handler Examples
CRUD API Handlers
// src/mocks/handlers/users.ts
import { http, HttpResponse, delay } from 'msw';
interface User {
id: string;
name: string;
email: string;
}
// In-memory store for testing
let users: User[] = [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' },
];
export const userHandlers = [
// List users with pagination
http.get('/api/users', ({ request }) => {
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page') || '1');
const limit = parseInt(url.searchParams.get('limit') || '10');
const start = (page - 1) * limit;
const paginatedUsers = users.slice(start, start + limit);
return HttpResponse.json({
data: paginatedUsers,
meta: {
page,
limit,
total: users.length,
totalPages: Math.ceil(users.length / limit),
},
});
}),
// Get single user
http.get('/api/users/:id', ({ params }) => {
const user = users.find((u) => u.id === params.id);
if (!user) {
return HttpResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
return HttpResponse.json({ data: user });
}),
// Create user
http.post('/api/users', async ({ request }) => {
const body = await request.json() as Omit<User, 'id'>;
const newUser: User = {
id: String(users.length + 1),
...body,
};
users.push(newUser);
return HttpResponse.json({ data: newUser }, { status: 201 });
}),
// Update user
http.put('/api/users/:id', async ({ request, params }) => {
const body = await request.json() as Partial<User>;
const index = users.findIndex((u) => u.id === params.id);
if (index === -1) {
return HttpResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
users[index] = { ...users[index], ...body };
return HttpResponse.json({ data: users[index] });
}),
// Delete user
http.delete('/api/users/:id', ({ params }) => {
const index = users.findIndex((u) => u.id === params.id);
if (index === -1) {
return HttpResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
users.splice(index, 1);
return new HttpResponse(null, { status: 204 });
}),
];Error Simulation Handlers
// src/mocks/handlers/errors.ts
import { http, HttpResponse, delay } from 'msw';
export const errorHandlers = [
// 401 Unauthorized
http.get('/api/protected', ({ request }) => {
const auth = request.headers.get('Authorization');
if (!auth || !auth.startsWith('Bearer ')) {
return HttpResponse.json(
{ error: 'Unauthorized', message: 'Missing or invalid token' },
{ status: 401 }
);
}
return HttpResponse.json({ data: 'secret data' });
}),
// 403 Forbidden
http.delete('/api/admin/users/:id', () => {
return HttpResponse.json(
{ error: 'Forbidden', message: 'Admin access required' },
{ status: 403 }
);
}),
// 422 Validation Error
http.post('/api/users', async ({ request }) => {
const body = await request.json() as { email?: string };
if (!body.email?.includes('@')) {
return HttpResponse.json(
{
error: 'Validation Error',
details: [
{ field: 'email', message: 'Invalid email format' },
],
},
{ status: 422 }
);
}
return HttpResponse.json({ data: { id: '1', ...body } }, { status: 201 });
}),
// 500 Server Error
http.get('/api/unstable', () => {
return HttpResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}),
// Network Error
http.get('/api/network-fail', () => {
return HttpResponse.error();
}),
// Timeout simulation
http.get('/api/timeout', async () => {
await delay('infinite');
return HttpResponse.json({ data: 'never' });
}),
];Authentication Flow Handlers
// src/mocks/handlers/auth.ts
import { http, HttpResponse } from 'msw';
interface LoginRequest {
email: string;
password: string;
}
const validUser = {
email: 'test@example.com',
password: 'password123',
};
export const authHandlers = [
// Login
http.post('/api/auth/login', async ({ request }) => {
const body = await request.json() as LoginRequest;
if (body.email === validUser.email && body.password === validUser.password) {
return HttpResponse.json({
user: { id: '1', email: body.email, name: 'Test User' },
accessToken: 'mock-access-token-123',
refreshToken: 'mock-refresh-token-456',
});
}
return HttpResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}),
// Refresh token
http.post('/api/auth/refresh', async ({ request }) => {
const body = await request.json() as { refreshToken: string };
if (body.refreshToken === 'mock-refresh-token-456') {
return HttpResponse.json({
accessToken: 'mock-access-token-new',
refreshToken: 'mock-refresh-token-new',
});
}
return HttpResponse.json(
{ error: 'Invalid refresh token' },
{ status: 401 }
);
}),
// Logout
http.post('/api/auth/logout', () => {
return new HttpResponse(null, { status: 204 });
}),
// Get current user
http.get('/api/auth/me', ({ request }) => {
const auth = request.headers.get('Authorization');
if (auth === 'Bearer mock-access-token-123' ||
auth === 'Bearer mock-access-token-new') {
return HttpResponse.json({
user: { id: '1', email: 'test@example.com', name: 'Test User' },
});
}
return HttpResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}),
];File Upload Handler
// src/mocks/handlers/upload.ts
import { http, HttpResponse } from 'msw';
export const uploadHandlers = [
http.post('/api/upload', async ({ request }) => {
const formData = await request.formData();
const file = formData.get('file') as File | null;
if (!file) {
return HttpResponse.json(
{ error: 'No file provided' },
{ status: 400 }
);
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!allowedTypes.includes(file.type)) {
return HttpResponse.json(
{ error: 'Invalid file type' },
{ status: 422 }
);
}
// Validate file size (5MB max)
if (file.size > 5 * 1024 * 1024) {
return HttpResponse.json(
{ error: 'File too large' },
{ status: 422 }
);
}
return HttpResponse.json({
data: {
id: 'file-123',
name: file.name,
size: file.size,
type: file.type,
url: `https://cdn.example.com/uploads/${file.name}`,
},
});
}),
];Test Usage Examples
Basic Component Test
// src/components/UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { server } from '../mocks/server';
import { UserList } from './UserList';
describe('UserList', () => {
it('renders users from API', async () => {
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('Bob')).toBeInTheDocument();
});
});
it('shows error state on API failure', async () => {
// Override handler for this test
server.use(
http.get('/api/users', () => {
return HttpResponse.json(
{ error: 'Server error' },
{ status: 500 }
);
})
);
render(<UserList />);
await waitFor(() => {
expect(screen.getByText(/error loading users/i)).toBeInTheDocument();
});
});
it('shows loading state during fetch', async () => {
server.use(
http.get('/api/users', async () => {
await delay(100);
return HttpResponse.json({ data: [] });
})
);
render(<UserList />);
expect(screen.getByTestId('loading-skeleton')).toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByTestId('loading-skeleton')).not.toBeInTheDocument();
});
});
});Form Submission Test
// src/components/CreateUserForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { server } from '../mocks/server';
import { CreateUserForm } from './CreateUserForm';
describe('CreateUserForm', () => {
it('submits form and shows success', async () => {
const user = userEvent.setup();
const onSuccess = vi.fn();
render(<CreateUserForm onSuccess={onSuccess} />);
await user.type(screen.getByLabelText('Name'), 'New User');
await user.type(screen.getByLabelText('Email'), 'new@example.com');
await user.click(screen.getByRole('button', { name: /create/i }));
await waitFor(() => {
expect(onSuccess).toHaveBeenCalledWith(
expect.objectContaining({ email: 'new@example.com' })
);
});
});
it('shows validation errors from API', async () => {
server.use(
http.post('/api/users', () => {
return HttpResponse.json(
{
error: 'Validation Error',
details: [{ field: 'email', message: 'Email already exists' }],
},
{ status: 422 }
);
})
);
const user = userEvent.setup();
render(<CreateUserForm onSuccess={() => {}} />);
await user.type(screen.getByLabelText('Email'), 'existing@example.com');
await user.click(screen.getByRole('button', { name: /create/i }));
await waitFor(() => {
expect(screen.getByText('Email already exists')).toBeInTheDocument();
});
});
});MSW 2.x API Reference
Core Imports
import { http, HttpResponse, graphql, ws, delay, passthrough } from 'msw';
import { setupServer } from 'msw/node';
import { setupWorker } from 'msw/browser';HTTP Handlers
Basic Methods
// GET request
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({ id: params.id, name: 'User' });
});
// POST request
http.post('/api/users', async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: 'new-123', ...body }, { status: 201 });
});
// PUT request
http.put('/api/users/:id', async ({ request, params }) => {
const body = await request.json();
return HttpResponse.json({ id: params.id, ...body });
});
// DELETE request
http.delete('/api/users/:id', ({ params }) => {
return new HttpResponse(null, { status: 204 });
});
// PATCH request
http.patch('/api/users/:id', async ({ request, params }) => {
const body = await request.json();
return HttpResponse.json({ id: params.id, ...body });
});
// Catch-all handler (NEW in 2.x)
http.all('/api/*', () => {
return HttpResponse.json({ error: 'Not implemented' }, { status: 501 });
});Response Types
// JSON response
HttpResponse.json({ data: 'value' });
HttpResponse.json({ data: 'value' }, { status: 201 });
// Text response
HttpResponse.text('Hello World');
// HTML response
HttpResponse.html('<h1>Hello</h1>');
// XML response
HttpResponse.xml('<root><item>value</item></root>');
// ArrayBuffer response
HttpResponse.arrayBuffer(buffer);
// FormData response
HttpResponse.formData(formData);
// No content
new HttpResponse(null, { status: 204 });
// Error response
HttpResponse.error();Headers and Cookies
http.get('/api/data', () => {
return HttpResponse.json(
{ data: 'value' },
{
headers: {
'X-Custom-Header': 'value',
'Set-Cookie': 'session=abc123; HttpOnly',
},
}
);
});Passthrough (NEW in 2.x)
Allow requests to pass through to the actual server:
import { passthrough } from 'msw';
// Passthrough specific endpoints
http.get('/api/health', () => passthrough());
// Conditional passthrough
http.get('/api/data', ({ request }) => {
if (request.headers.get('X-Bypass-Mock') === 'true') {
return passthrough();
}
return HttpResponse.json({ mocked: true });
});Delay Simulation
import { delay } from 'msw';
http.get('/api/slow', async () => {
await delay(2000); // 2 second delay
return HttpResponse.json({ data: 'slow response' });
});
// Realistic delay (random between min and max)
http.get('/api/realistic', async () => {
await delay('real'); // 100-400ms random delay
return HttpResponse.json({ data: 'response' });
});
// Infinite delay (useful for testing loading states)
http.get('/api/hang', async () => {
await delay('infinite');
return HttpResponse.json({ data: 'never reaches' });
});GraphQL Handlers
import { graphql } from 'msw';
// Query
graphql.query('GetUser', ({ variables }) => {
return HttpResponse.json({
data: {
user: {
id: variables.id,
name: 'Test User',
},
},
});
});
// Mutation
graphql.mutation('CreateUser', ({ variables }) => {
return HttpResponse.json({
data: {
createUser: {
id: 'new-123',
...variables.input,
},
},
});
});
// Error response
graphql.query('GetUser', () => {
return HttpResponse.json({
errors: [{ message: 'User not found' }],
});
});
// Scoped to endpoint
const github = graphql.link('https://api.github.com/graphql');
github.query('GetRepository', ({ variables }) => {
return HttpResponse.json({
data: {
repository: { name: variables.name },
},
});
});WebSocket Handlers (NEW in 2.x)
import { ws } from 'msw';
const chat = ws.link('wss://api.example.com/chat');
export const wsHandlers = [
chat.addEventListener('connection', ({ client }) => {
// Send welcome message
client.send(JSON.stringify({ type: 'welcome', message: 'Connected!' }));
// Handle incoming messages
client.addEventListener('message', (event) => {
const data = JSON.parse(event.data.toString());
if (data.type === 'ping') {
client.send(JSON.stringify({ type: 'pong' }));
}
});
// Handle close
client.addEventListener('close', () => {
console.log('Client disconnected');
});
}),
];Server Setup (Node.js/Vitest)
// src/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// vitest.setup.ts
import { beforeAll, afterEach, afterAll } from 'vitest';
import { server } from './src/mocks/server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());Browser Setup (Storybook/Dev)
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// Start in development
if (process.env.NODE_ENV === 'development') {
worker.start({
onUnhandledRequest: 'bypass',
});
}Request Info Access
http.post('/api/data', async ({ request, params, cookies }) => {
// Request body
const body = await request.json();
// URL parameters
const { id } = params;
// Query parameters
const url = new URL(request.url);
const page = url.searchParams.get('page');
// Headers
const auth = request.headers.get('Authorization');
// Cookies
const session = cookies.session;
return HttpResponse.json({ received: body });
});External Links
Create MSW handler for: $ARGUMENTS
Handler Context (Auto-Detected)
- MSW Version: !
grep -r "msw" package.json 2>/dev/null | head -1 | grep -oE 'msw[^"]*' || echo "Not detected" - Existing Handlers: !
grep -r "rest\.get\|rest\.post" src/mocks tests/mocks 2>/dev/null | wc -l | tr -d ' ' || echo "0" - API Base URL: !
grep -r "API_URL\|VITE_API\|NEXT_PUBLIC_API" .env* 2>/dev/null | head -1 | cut -d'=' -f2 || echo "/api" - Handlers Location: !
find . -type d -name "mocks" -o -name "handlers" 2>/dev/null | head -1 || echo "src/mocks"
MSW Handler Template
/**
* MSW Handler: $ARGUMENTS
*
* Generated: !`date +%Y-%m-%d`
* Endpoint: $ARGUMENTS
*/
import { http, HttpResponse, delay } from 'msw';
export const handlers = [
http.get('$ARGUMENTS', async () => {
await delay(100); // Simulate network delay
return HttpResponse.json({
data: [],
// Add your mock data here
});
}),
http.post('$ARGUMENTS', async ({ request }) => {
const body = await request.json();
return HttpResponse.json({
id: '123',
...body,
}, { status: 201 });
}),
];Usage
1. Review detected patterns above 2. Add to: src/mocks/handlers.ts 3. Register in MSW setup
/**
* MSW Handler Template
*
* Copy this template when creating new API handlers.
* Replace placeholders with actual types and data.
*/
import { http, HttpResponse, delay } from 'msw';
// =============================================================================
// Types
// =============================================================================
interface Resource {
id: string;
name: string;
createdAt: string;
updatedAt: string;
}
interface CreateResourceRequest {
name: string;
}
interface UpdateResourceRequest {
name?: string;
}
interface PaginatedResponse<T> {
data: T[];
meta: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
interface ErrorResponse {
error: string;
message?: string;
details?: Array<{ field: string; message: string }>;
}
// =============================================================================
// Mock Data Store
// =============================================================================
let resources: Resource[] = [
{
id: '1',
name: 'Resource 1',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
},
{
id: '2',
name: 'Resource 2',
createdAt: '2024-01-02T00:00:00Z',
updatedAt: '2024-01-02T00:00:00Z',
},
];
// =============================================================================
// Handlers
// =============================================================================
export const resourceHandlers = [
// -------------------------------------------------------------------------
// LIST - GET /api/resources
// -------------------------------------------------------------------------
http.get('/api/resources', ({ request }) => {
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page') || '1');
const limit = parseInt(url.searchParams.get('limit') || '10');
const search = url.searchParams.get('search') || '';
// Filter by search
let filtered = resources;
if (search) {
filtered = resources.filter((r) =>
r.name.toLowerCase().includes(search.toLowerCase())
);
}
// Paginate
const start = (page - 1) * limit;
const paginated = filtered.slice(start, start + limit);
const response: PaginatedResponse<Resource> = {
data: paginated,
meta: {
page,
limit,
total: filtered.length,
totalPages: Math.ceil(filtered.length / limit),
},
};
return HttpResponse.json(response);
}),
// -------------------------------------------------------------------------
// GET ONE - GET /api/resources/:id
// -------------------------------------------------------------------------
http.get('/api/resources/:id', ({ params }) => {
const resource = resources.find((r) => r.id === params.id);
if (!resource) {
return HttpResponse.json(
{ error: 'Not Found', message: 'Resource not found' } as ErrorResponse,
{ status: 404 }
);
}
return HttpResponse.json({ data: resource });
}),
// -------------------------------------------------------------------------
// CREATE - POST /api/resources
// -------------------------------------------------------------------------
http.post('/api/resources', async ({ request }) => {
const body = (await request.json()) as CreateResourceRequest;
// Validation
if (!body.name || body.name.length < 2) {
return HttpResponse.json(
{
error: 'Validation Error',
details: [{ field: 'name', message: 'Name must be at least 2 characters' }],
} as ErrorResponse,
{ status: 422 }
);
}
const newResource: Resource = {
id: String(resources.length + 1),
name: body.name,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
resources.push(newResource);
return HttpResponse.json({ data: newResource }, { status: 201 });
}),
// -------------------------------------------------------------------------
// UPDATE - PUT /api/resources/:id
// -------------------------------------------------------------------------
http.put('/api/resources/:id', async ({ request, params }) => {
const body = (await request.json()) as UpdateResourceRequest;
const index = resources.findIndex((r) => r.id === params.id);
if (index === -1) {
return HttpResponse.json(
{ error: 'Not Found', message: 'Resource not found' } as ErrorResponse,
{ status: 404 }
);
}
resources[index] = {
...resources[index],
...body,
updatedAt: new Date().toISOString(),
};
return HttpResponse.json({ data: resources[index] });
}),
// -------------------------------------------------------------------------
// DELETE - DELETE /api/resources/:id
// -------------------------------------------------------------------------
http.delete('/api/resources/:id', ({ params }) => {
const index = resources.findIndex((r) => r.id === params.id);
if (index === -1) {
return HttpResponse.json(
{ error: 'Not Found', message: 'Resource not found' } as ErrorResponse,
{ status: 404 }
);
}
resources.splice(index, 1);
return new HttpResponse(null, { status: 204 });
}),
];
// =============================================================================
// Test Helper: Reset Store
// =============================================================================
export function resetResourceStore() {
resources = [
{
id: '1',
name: 'Resource 1',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
},
{
id: '2',
name: 'Resource 2',
createdAt: '2024-01-02T00:00:00Z',
updatedAt: '2024-01-02T00:00:00Z',
},
];
}