How to add an API endpoint
A goal-oriented guide for adding a new endpoint to the frontend's API surface. This covers the frontend side only.
Before you start
You need to know:
- The endpoint's URL (e.g.
/api/listening-streaks) - Its query parameters (if any)
- The response shape
If the backend exists, get this from the backend team. If it doesn't yet, define the contract and mock it.
1. Define the response type
Open src/types/api.ts and add an interface for the response. Match the backend's actual JSON keys exactly.
export interface ListeningStreak {
startDate: string;
endDate: string;
consecutiveDays: number;
}
export interface ListeningStreaksResponse {
pagination: Pagination;
streaks: ListeningStreak[];
}2. Add the service function
In src/services/api.ts:
/**
* Fetches the user's longest listening streaks
*/
export async function fetchListeningStreaks(
limit = 10,
offset = 0
): Promise<ListeningStreaksResponse> {
return apiFetch('/api/listening-streaks', buildParams({ limit, offset }));
}Conventions:
- One service file per API domain. If you're adding several related endpoints, group them in the same file.
- Always use
apiFetchas it handles error responses, JSON parsing, and the auth cookie automatically. - Use
buildParamsfor query strings as it skips undefined values.
3. Add an MSW handler
Without a handler, the service will fail in dev and tests. Add a fixture in src/mocks/fixtures.ts:
export const listeningStreaksFixture: ListeningStreaksResponse = {
pagination: { total: 1, limit: 10, offset: 0 },
streaks: [{ startDate: '2024-06-01', endDate: '2024-06-14', consecutiveDays: 14 }],
};And a handler in src/mocks/handlers.ts:
http.get('/api/listening-streaks', () => HttpResponse.json(listeningStreaksFixture)),4. (Usually) add a TanStack Query hook
If the endpoint will be consumed by a component, wrap it in a hook. Pure backend-driven services that aren't called from React don't need this step.
export function useListeningStreaks() {
return useQuery({
queryKey: ['listening-streaks'],
queryFn: () => fetchListeningStreaks(),
});
}5. Test the service
At minimum, verify that the success path returns the expected shape:
it('fetches listening streaks', async () => {
const result = await fetchListeningStreaks();
expect(result.streaks).toHaveLength(1);
expect(result.streaks[0].consecutiveDays).toBe(14);
});Done
The endpoint is now part of the frontend's surface. Anything that imports the hook or service can use it without thinking about HTTP.