6.6 KiB
Mock API Guide
Table of Contents
Structure
Each mock API file lives in src/constants/mock-api-<name>.ts and is a self-contained in-memory database. It uses:
- faker for generating sample data
- match-sorter for fuzzy search across fields
- delay (from
./mock-api) to simulate network latency
The delay function is exported from src/constants/mock-api.ts:
export async function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Full Template
import { faker } from '@faker-js/faker';
import { matchSorter } from 'match-sorter';
import { delay } from './mock-api';
// 1. Define the entity type
export type Order = {
id: number;
customer: string;
email: string;
status: string;
total: number;
created_at: string;
updated_at: string;
};
// 2. Create the fake database object
export const fakeOrders = {
records: [] as Order[],
// 3. Initialize with faker data
initialize() {
const statuses = ['pending', 'processing', 'completed', 'cancelled'];
for (let i = 1; i <= 20; i++) {
this.records.push({
id: i,
customer: faker.person.fullName(),
email: faker.internet.email(),
status: faker.helpers.arrayElement(statuses),
total: parseFloat(faker.commerce.price({ min: 10, max: 500 })),
created_at: faker.date.between({ from: '2023-01-01', to: Date.now() }).toISOString(),
updated_at: faker.date.recent().toISOString()
});
}
},
// 4. Get all with optional search (used internally)
async getAll({ search }: { search?: string } = {}) {
let items = [...this.records];
if (search) {
items = matchSorter(items, search, {
keys: ['customer', 'email']
});
}
return items;
},
// 5. Paginated list with filtering and sorting
async getOrders(params: {
page?: number;
limit?: number;
search?: string;
statuses?: string;
sort?: string;
}) {
await delay(800);
const { page = 1, limit = 10, search, statuses, sort } = params;
let items = await this.getAll({ search });
// Filter by comma-separated values
if (statuses) {
const statusList = statuses.split('.');
items = items.filter((item) => statusList.includes(item.status));
}
// Sort by column
if (sort) {
const parsedSort = JSON.parse(sort) as { id: string; desc: boolean }[];
if (parsedSort.length > 0) {
const { id, desc } = parsedSort[0];
items.sort((a, b) => {
const aVal = a[id as keyof Order];
const bVal = b[id as keyof Order];
if (aVal < bVal) return desc ? 1 : -1;
if (aVal > bVal) return desc ? -1 : 1;
return 0;
});
}
}
// Paginate
const total_items = items.length;
items = items.slice((page - 1) * limit, page * limit);
return { items, total_items };
},
// 6. Get single record by ID
async getOrderById(id: number) {
await delay(800);
return this.records.find((r) => r.id === id) || null;
},
// 7. Create
async createOrder(data: Omit<Order, 'id' | 'created_at' | 'updated_at'>) {
await delay(800);
const newRecord: Order = {
...data,
id: this.records.length + 1,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
this.records.push(newRecord);
return newRecord;
},
// 8. Update
async updateOrder(id: number, data: Partial<Order>) {
await delay(800);
const idx = this.records.findIndex((r) => r.id === id);
if (idx === -1) return null;
this.records[idx] = {
...this.records[idx],
...data,
updated_at: new Date().toISOString()
};
return this.records[idx];
},
// 9. Delete
async deleteOrder(id: number) {
await delay(800);
this.records = this.records.filter((r) => r.id !== id);
return true;
}
};
// 10. Auto-initialize on import
fakeOrders.initialize();
Key Patterns
Search with match-sorter
Always specify which fields to search across:
matchSorter(items, search, { keys: ['customer', 'email', 'status'] });
Comma-separated filter values
For multi-select filters (roles, statuses), the URL param uses . as delimiter:
if (statuses) {
const list = statuses.split('.');
items = items.filter((item) => list.includes(item.status));
}
Computed column sorting
When a table has a computed column (e.g., combining first_name + last_name into "name"), handle it in the sort logic:
if (id === 'name') {
const aName = `${a.first_name} ${a.last_name}`;
const bName = `${b.first_name} ${b.last_name}`;
return desc ? bName.localeCompare(aName) : aName.localeCompare(bName);
}
Return shape
List methods must return { items, total_items } (or { products, total } etc. — match the query option expectations). The total is the count before pagination, used for pageCount calculation.
Integrating with the API Layer
The mock API is only imported in service.ts. Queries and components import from the service and types files:
mock-api-orders.ts → api/service.ts → api/queries.ts → components
(data source) (data access) (key factory + (useSuspenseQuery
queryOptions) + useMutation)
service.ts imports from the mock API:
import { fakeOrders } from '@/constants/mock-api-orders';
import type { OrderFilters, OrdersResponse } from './types';
export async function getOrders(filters: OrderFilters): Promise<OrdersResponse> {
return fakeOrders.getOrders(filters);
}
export async function createOrder(data: OrderMutationPayload) {
return fakeOrders.createOrder(data);
}
queries.ts imports from service, uses key factories:
import { getOrders } from './service';
import type { OrderFilters } from './types';
export const orderKeys = {
all: ['orders'] as const,
list: (filters: OrderFilters) => [...orderKeys.all, 'list', filters] as const,
detail: (id: number) => [...orderKeys.all, 'detail', id] as const
};
export const ordersQueryOptions = (filters: OrderFilters) =>
queryOptions({ queryKey: orderKeys.list(filters), queryFn: () => getOrders(filters) });
Mutations in components use service functions + key factories:
import { createOrder } from '../api/service';
import { orderKeys } from '../api/queries';
const mutation = useMutation({
mutationFn: (data) => createOrder(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: orderKeys.all })
});