167 lines
5.2 KiB
Markdown
167 lines
5.2 KiB
Markdown
# @repo/api-client -- Framework-Agnostic tRPC + React Query Provider
|
|
|
|
## Purpose
|
|
|
|
This package provides a framework-agnostic tRPC client and React Query provider that any frontend app (Next.js, TanStack Start, or future frameworks) can consume. It exposes `<ApiProvider>` for initialization and `useTRPC()` for fully typed data fetching. It contains zero business logic.
|
|
|
|
## Hard Rules
|
|
|
|
- **NEVER** import framework-specific code (no `next/`, no `@tanstack/start`, no `vinxi/`)
|
|
- **NEVER** put business logic in this package
|
|
- **NEVER** create custom hooks that duplicate what `useTRPC()` already provides
|
|
- Both Next.js and TanStack Start apps use the same `<ApiProvider>` and `useTRPC()`
|
|
- This package depends on `@repo/api` for the `AppRouter` type only (no runtime import)
|
|
|
|
## File Structure
|
|
|
|
```
|
|
packages/api-client/
|
|
src/
|
|
trpc.ts # Creates TRPCProvider + useTRPC via createTRPCContext<AppRouter>()
|
|
query-client.ts # Singleton QueryClient factory (SSR-safe)
|
|
provider.tsx # <ApiProvider> component: wires tRPC client + QueryClientProvider
|
|
index.ts # Package entry: re-exports ApiProvider, useTRPC, getQueryClient
|
|
package.json
|
|
AGENTS.md
|
|
```
|
|
|
|
## How Apps Consume This Package
|
|
|
|
### Step 1: Wrap your app with `<ApiProvider>`
|
|
|
|
The provider needs a `trpcUrl` pointing to the tRPC HTTP endpoint:
|
|
|
|
```tsx
|
|
// apps/web-next/src/app/providers.tsx
|
|
"use client";
|
|
|
|
import { ApiProvider } from "@repo/api-client";
|
|
|
|
export function Providers({ children }: { children: React.ReactNode }) {
|
|
return <ApiProvider trpcUrl="/api/trpc">{children}</ApiProvider>;
|
|
}
|
|
```
|
|
|
|
```tsx
|
|
// apps/web-next/src/app/layout.tsx
|
|
import { Providers } from "./providers";
|
|
|
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<html lang="en">
|
|
<body>
|
|
<Providers>{children}</Providers>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|
|
```
|
|
|
|
For TanStack Start, the provider goes in the root route:
|
|
|
|
```tsx
|
|
// apps/web-tanstack/src/routes/__root.tsx
|
|
import { Outlet, createRootRoute } from "@tanstack/react-router";
|
|
import { ApiProvider } from "@repo/api-client";
|
|
|
|
export const Route = createRootRoute({
|
|
component: () => (
|
|
<ApiProvider trpcUrl="http://localhost:3000/api/trpc">
|
|
<Outlet />
|
|
</ApiProvider>
|
|
),
|
|
});
|
|
```
|
|
|
|
### Step 2: Use `useTRPC()` in components
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import { useTRPC } from "@repo/api-client";
|
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
|
|
|
export function ArticleList() {
|
|
const trpc = useTRPC();
|
|
|
|
// Query -- reads data
|
|
const { data, isLoading, error } = useQuery(
|
|
trpc.content.listArticles.queryOptions({ status: "published", limit: 10 })
|
|
);
|
|
|
|
// Mutation -- writes data
|
|
const createArticle = useMutation(
|
|
trpc.content.createArticle.mutationOptions()
|
|
);
|
|
|
|
if (isLoading) return <p>Loading...</p>;
|
|
if (error) return <p>Error: {error.message}</p>;
|
|
|
|
return (
|
|
<div>
|
|
<ul>
|
|
{data?.map((article) => (
|
|
<li key={article.id}>{article.title}</li>
|
|
))}
|
|
</ul>
|
|
<button
|
|
onClick={() =>
|
|
createArticle.mutate({
|
|
title: "New Article",
|
|
content: "Hello world",
|
|
authorId: "user-1",
|
|
})
|
|
}
|
|
>
|
|
Create Article
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
## The `useTRPC()` Pattern
|
|
|
|
`useTRPC()` is created by `createTRPCContext<AppRouter>()` from `@trpc/tanstack-react-query`. It returns a proxy object that mirrors the router structure:
|
|
|
|
```
|
|
useTRPC()
|
|
.auth
|
|
.signIn.mutationOptions()
|
|
.signUp.mutationOptions()
|
|
.signOut.mutationOptions()
|
|
.content
|
|
.listArticles.queryOptions({ ... })
|
|
.createArticle.mutationOptions()
|
|
```
|
|
|
|
You pass `.queryOptions()` to `useQuery()` and `.mutationOptions()` to `useMutation()` from `@tanstack/react-query`. This gives you full control over caching, refetching, optimistic updates, and all React Query features.
|
|
|
|
## Custom Hook Wrappers Are Optional
|
|
|
|
Since `useTRPC()` gives fully typed access to every procedure, you do **not** need to create wrapper hooks like `useArticles()`. Only create a custom hook if you have shared logic (e.g., combining multiple queries, adding retry logic, or transforming results) that would otherwise be duplicated across multiple components.
|
|
|
|
## QueryClient Configuration
|
|
|
|
The `getQueryClient()` factory in `query-client.ts` handles SSR correctly:
|
|
- **Server-side:** Creates a new `QueryClient` per request (avoids cross-request data leaks)
|
|
- **Client-side:** Returns a singleton `QueryClient` (reused across renders)
|
|
- Default `staleTime` is 30 seconds
|
|
|
|
## Dependencies
|
|
|
|
| Dependency | Purpose |
|
|
|---|---|
|
|
| `@repo/api` | `AppRouter` type for end-to-end type safety (type-only import) |
|
|
| `@trpc/client` | tRPC client with `httpBatchLink` |
|
|
| `@trpc/tanstack-react-query` | `createTRPCContext` for React Query integration |
|
|
| `@tanstack/react-query` | `QueryClient`, `QueryClientProvider` |
|
|
| `react` | JSX runtime for provider component |
|
|
|
|
## Cross-References
|
|
|
|
- **Router types come from:** `packages/api/` -- see `packages/api/AGENTS.md`
|
|
- **tRPC HTTP endpoint:** `apps/web-next/src/app/api/trpc/[trpc]/route.ts`
|
|
- **Provider usage in Next.js:** `apps/web-next/src/app/providers.tsx`
|
|
- **Provider usage in TanStack:** `apps/web-tanstack/src/routes/__root.tsx`
|