# Quick start

> An AtomRpc client, atoms with serialization keys, a router that streams the registry, a loader and a boundary, in a TanStack Start app.

This page builds one route of a [TanStack Start](https://tanstack.com/start/latest) app on an Effect RPC contract: an issue tracker's `IssueRpcs` group, whose `GetIssue` fails with `IssueNotFound` and whose every procedure can fail with `Unauthorized`.

## 1. The client

`AtomRpc.Service` makes the RPC client a service with query atoms. During server rendering, the transport calls the RPC handler in the same process; in the browser it posts to the app's `/rpc` route.

```ts title="src/issues-client.ts"
export class IssuesClient extends AtomRpc.Service<IssuesClient>()(
	"IssuesClient",
	{
		group: IssueRpcs,
		protocol: RpcClient.layerProtocolHttp({ url: rpcUrl }).pipe(
			Layer.provide([RpcHttpClient, RpcSerialization.layerJson]),
		),
	},
) {}
```

## 2. The atoms

A query atom with a `serializationKey` can cross from the server to the browser. Without one, the browser requests it again.

```ts title="src/issues.ts"
// A serialization key lets the server send the result to the browser.
export const issue = (id: IssueId) =>
	// const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>
	IssuesClient.query("GetIssue", { id }, { serializationKey: `${id}` });

export const assignee = (issueId: IssueId) =>
	IssuesClient.query(
		"GetIssueAssignee",
		{ issueId },
		{ serializationKey: `${issueId}` },
	);
```

## 3. The router

`makeRegistry` makes one registry per request on the server and one for the page in the browser. `setupRouterAtoms` provides it to the app and sends it to the browser with the router's own dehydrated state.

```tsx title="src/router.tsx"
export function getRouter() {
	// A request registry on the server, the browser's registry in the browser.
	const registry = makeRegistry();
	const router = createRouter({
		routeTree,
		context: { atoms: Loader.make(registry) },
		defaultPreload: "intent",
	});
	setupRouterAtoms({ router, registry });
	return router;
}
```

The root route types the loader verbs on the router context and mounts the app's boundaries.

```tsx title="src/routes/__root.tsx"
export const Route = createRootRouteWithContext<{ atoms: Loader.Loader }>()({
	component: () => (
		// In development, a component that starts an atom no loader started throws.
		<WaterfallDetection mode="throw">
			<AppBoundary>
				<AuthBoundary>
					<Outlet />
				</AuthBoundary>
			</AppBoundary>
		</WaterfallDetection>
	),
});
```

## 4. The boundaries

Each boundary catches failures by tag, and passes the rest up the chain: the issue boundary catches `IssueNotFound`, the sign-in boundary `Unauthorized`, and the app boundary Effect RPC's [`RpcClientError`](https://github.com/Effect-TS/effect/blob/effect%404.0.6/packages/effect/src/rpc/RpcClientError.ts), which every procedure fails with when the request doesn't reach the server. See [Shared boundaries](https://raf-effect-atom.katt.dev/docs/shared-boundaries.md).

```tsx title="src/boundaries.tsx"
// Every procedure fails with RpcClientError when the request doesn't reach
// the server, so the app catches it once.
export const [AppBoundary, useAppBoundary] = createErrorBoundary({
	name: "App",
	errors: IssuesClient,
	catchTags: {
		RpcClientError: ({ reset }) => (
			<p>
				Can't reach the server. <button onClick={reset}>Try again</button>
			</p>
		),
	},
});
```

```tsx title="src/boundaries.tsx"
// Under the App boundary: every procedure can fail with Unauthorized too.
export const [AuthBoundary, useAuthBoundary] = createErrorBoundary({
	name: "Auth",
	errors: IssuesClient,
	parent: AppBoundary,
	catchTags: {
		Unauthorized: ({ reset }) => (
			<p>
				You're signed out. <button onClick={reset}>Try again</button>
			</p>
		),
	},
});
```

```tsx title="src/boundaries.tsx"
// Under the Auth boundary: it claims IssueNotFound, and Unauthorized and
// RpcClientError still go up the chain.
export const [IssueBoundary, useIssueBoundary] = createErrorBoundary({
	name: "Issue",
	errors: IssuesClient,
	parent: AuthBoundary,
	catchTags: {
		IssueNotFound: ({ error }) => <p>There's no issue #{error.id}.</p>,
	},
});
```

## 5. The route

```tsx title="src/routes/issues.$id.tsx"
export const Route = createFileRoute("/issues/$id")({
	params: {
		parse: (params) => Schema.decodeUnknownSync(IssueParams)(params),
		stringify: Schema.encodeSync(IssueParams),
	},
	loader: async ({ context, params }) => ({
		// Starts the assignee; it streams in while the page renders.
		assignee: context.atoms.prefetch(assignee, params.id),
		// Waits for the issue, so the page renders with it.
		issue: await context.atoms.ensure(issue, params.id),
	}),
	component: () => (
		<IssueBoundary>
			<IssuePage />
		</IssueBoundary>
	),
});
```

```tsx title="src/routes/issues.$id.tsx"
function IssuePage() {
	const data = Route.useLoaderData();
	// IssueNotFound, Unauthorized and RpcClientError go to their boundaries,
	// so no failure is left.
	const result = useIssueBoundary(useAtomResult(issue, data.issue));
	// const result: Success<Issue, never>
	return (
		<article>
			<h1>{result.value.title}</h1>
			<Suspense fallback={<p>Loading the assignee…</p>}>
				<Assignee assignee={data.assignee} />
			</Suspense>
		</article>
	);
}

// Takes the handle the loader returned, not an id: it reads a request that
// already started, and can't start its own.
function Assignee(props: { assignee: Handle<typeof assignee> }) {
	const result = useIssueBoundary(useAtomResult(assignee, props.assignee));
	return <p>{result.value.name}</p>;
}
```

Open `/issues/1`: the server waits for the issue, renders the title, and streams the assignee in when it arrives. The browser reads both from its registry without requesting them again. Open `/issues/999` and the server sends the `IssueNotFound` failure instead: the browser renders the issue boundary's message from it, still without a request.
