Skip to content

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.

ts
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:

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 apiFetch as it handles error responses, JSON parsing, and the auth cookie automatically.
  • Use buildParams for 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:

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:

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.

ts
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:

ts
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.

Built for SE_07 — Technical Documentation