-
Read the current ApiClient file at src/renderer/utils/api.ts to confirm the existing interface naming, the request<T>() signature, and the section ordering (auth methods → libraries → items → playback → search). New methods must be inserted into the matching section, not appended blindly. Verify the file still exposes export const apiClient = new ApiClient() at the bottom before proceeding.
-
Define the response interface(s) above the class. Match the existing style — PascalCase, one field per line, optional fields with ?, no inline JSDoc unless the field name is genuinely ambiguous. Mirror neighbors like MediaItem, Library, AuthResult. If the endpoint returns a paginated list, model it as { items: Thing[]; total: number; offset: number; limit: number } to match getLibraryItems's return shape. Verify the interface is exported before proceeding.
-
Define a request interface only if the body has more than 2 fields or any optional fields. For 1–2 required primitives, accept them as positional method parameters (see login(username, password)). For richer payloads, define export interface FooRequest { ... } and accept a single data: FooRequest parameter (see how complex payloads are passed as data to this.request). Verify by checking that the resulting method signature matches the verbosity of the nearest existing method.
-
Add the method inside the ApiClient class in the section matching its concern (auth, libraries, items, playback, search). Use this exact shape — adjust verb, path, generics, and body:
async getThing(id: string): Promise<Thing> {
return this.request<Thing>('GET', `/things/${id}`);
}
async createThing(data: CreateThingRequest): Promise<Thing> {
return this.request<Thing>('POST', '/things', data);
}
async listThings(libraryId: string, offset = 0, limit = 50): Promise<ThingList> {
const params = new URLSearchParams({
library_id: libraryId,
offset: offset.toString(),
limit: limit.toString(),
});
return this.request<ThingList>('GET', `/things?${params.toString()}`);
}
Verify: the method calls this.request<T>() exactly once, the generic <T> matches the declared return type, and the path begins with / and does NOT include /api/v1.
-
Use URLSearchParams for ALL GET query strings, even single-param ones, to match getLibraryItems in src/renderer/utils/api.ts. Coerce numbers with .toString() and booleans with String(bool). Skip falsy/undefined params with a conditional if (foo !== undefined) params.append('foo', String(foo)); — do not send empty strings. Verify the final path renders correctly by mentally substituting sample values.
-
For non-GET requests that take a body, pass the body as the third argument to this.request(). request() forwards it as data to axios, which JSON-encodes it automatically because the constructor sets 'Content-Type': 'application/json'. Do NOT JSON.stringify the body yourself. Do NOT add headers — the interceptors handle auth.
-
Do NOT add try/catch inside the method. The axios response interceptor in the constructor already handles 401 by clearing the token, emitting auth:logout, and rejecting. Callers (stores, pages) are responsible for their own error handling — let the promise reject so they can catch it. Adding a local try/catch swallows errors that stores rely on.
-
Update the consuming store or page. Find the zustand store in src/renderer/stores/ that owns this domain (e.g. playbackStore.ts for playback endpoints) and add an action that calls apiClient.yourNewMethod(...). Import the response interface from ../utils/api. If no store fits, call apiClient directly from the page component in src/renderer/pages/. Verify the store/page compiles by running step 9.
-
Type-check and test. Run these and confirm both pass before claiming completion:
npx tsc --noEmit
npm test -- tests/unit/api.test.ts
If a relevant test exists in tests/unit/api.test.ts, extend it with a case for the new method that mocks the underlying axios client and asserts the URL, method, and body. If no matching test pattern exists for this domain yet, add one following the existing test's mocking style.