Phase 2: API client, typed hooks, live probe

- Add api:types script (openapi-typescript against live FastAPI)
- Generate src/lib/api/types.ts (2972 lines, 55 paths, 16 schemas)
- lib/api/client.ts: typed apiRequest + ApiError + makeQueryClient
  (staleTime 5s, no refetchOnWindowFocus to preserve editor state)
- lib/providers.tsx: QueryClientProvider client component, useState
  singleton so App Router re-renders don't dump the cache
- lib/api/cases.ts: Case type + casesKeys + useCases hook (pragmatic
  hand-typed Case pending backend response-model annotations)
- layout.tsx: wrap children with <Providers>
- Smoke test: CasesLiveProbe component on home page hitting live FastAPI
  via /api/cases rewrite proxy

Phase 2 deliverable check: useCases() returns typed Case[] from the
production FastAPI through the Next.js proxy. End-to-end wiring proven.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chaim
2026-04-11 15:49:24 +00:00
parent 64724656af
commit 0ee8e723bd
9 changed files with 3260 additions and 80 deletions

View File

@@ -0,0 +1,77 @@
/**
* API client — typed fetch wrapper + TanStack Query setup.
*
* All requests hit relative URLs (e.g. `/api/cases`) which next.config.ts
* rewrites transparently proxy to legal-ai.nautilus.marcusgroup.org. No CORS
* gymnastics, no direct public-URL references in component code.
*/
import { QueryClient } from "@tanstack/react-query";
export class ApiError extends Error {
constructor(
message: string,
public readonly status: number,
public readonly body: unknown,
) {
super(message);
this.name = "ApiError";
}
}
type RequestOptions = {
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
body?: unknown;
signal?: AbortSignal;
};
/**
* Typed JSON request. Throws ApiError on non-2xx responses with the parsed body.
* Always returns parsed JSON — callers pass the expected type parameter.
*/
export async function apiRequest<T>(
path: string,
{ method = "GET", body, signal }: RequestOptions = {},
): Promise<T> {
const res = await fetch(path, {
method,
headers: body ? { "Content-Type": "application/json" } : undefined,
body: body ? JSON.stringify(body) : undefined,
signal,
});
const contentType = res.headers.get("content-type") ?? "";
const parsed = contentType.includes("application/json")
? await res.json().catch(() => null)
: await res.text().catch(() => null);
if (!res.ok) {
throw new ApiError(
`${method} ${path} failed with ${res.status}`,
res.status,
parsed,
);
}
return parsed as T;
}
/**
* Shared TanStack Query client. Defaults are tuned for a dashboard that polls
* for case progress — stale for 5 seconds, 1 automatic retry, no background
* refetch on window focus (preserves editor state during long sessions).
*/
export function makeQueryClient(): QueryClient {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 5_000,
retry: 1,
refetchOnWindowFocus: false,
},
mutations: {
retry: 0,
},
},
});
}