- 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>
78 lines
2.0 KiB
TypeScript
78 lines
2.0 KiB
TypeScript
/**
|
|
* 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,
|
|
},
|
|
},
|
|
});
|
|
}
|