> ## Documentation Index
> Fetch the complete documentation index at: https://ship.paralect.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Calling the API

> Consume the typed oRPC client from the web app with useApiQuery and useApiMutation — query keys derived from the client itself, zero hand-written hooks.

In a full-stack project the web app talks to `apps/api` through a fully typed [oRPC](https://orpc.unnoq.com/) client. You call endpoints as plain methods — `apiClient.users.getCurrent()` — and wrap them in [TanStack Query](https://tanstack.com/query) with two thin hooks: `useApiQuery` and `useApiMutation`.

<Info>
  The oRPC client and these hooks ship with the **Auth plugin** (`auth-starter`). Install it and `apps/web` gains the client, the hooks, and the authenticated app shell. The base web app has no API wiring.
</Info>

## The client

The client lives in `@/services/api-client.service.ts`. It's an oRPC client typed by the API's exported `AppClient`, so every resource, method, input and output is real — change an endpoint's `.output()` and the call site's types update on the next API type build.

```ts theme={null}
import { apiClient } from '@/services/api-client.service';

// resource → method → call
const user = await apiClient.users.getCurrent();
const page = await apiClient.users.list({ page: 1, perPage: 20 });
```

The shape mirrors the API directory: `apiClient.<resource>.<method>(input)`. Method names are the codegen-derived names from the contract — `users.getCurrent`, `users.patchCurrent`, `users.list`, `files.upload`, `files.getUrl`, `files.remove`.

## Queries

`useApiQuery` takes a procedure and an optional input. It derives the query key from the procedure itself, so you never hand-write keys.

```tsx theme={null}
import { useApiQuery } from '@/hooks';
import { apiClient } from '@/services/api-client.service';

// no input
const { data: user, isLoading } = useApiQuery(apiClient.users.getCurrent);

// with input
const { data: page } = useApiQuery(apiClient.users.list, { page: 1, perPage: 20 });

// with TanStack Query options
const { data } = useApiQuery(apiClient.users.list, { page: 1 }, { staleTime: 30_000 });
```

`data` is typed from the endpoint's `.output()` schema. When the procedure takes no input you can pass query options as the second argument directly — the hook tells an input apart from an options object for you.

## Mutations

`useApiMutation` wraps a procedure in `useMutation`. The error type is `ORPCError` (an `Error` with optional `data` and `status`), which is exactly what [`handleApiError`](/docs/web/forms) expects.

```tsx theme={null}
import { useApiMutation } from '@/hooks';
import { apiClient } from '@/services/api-client.service';
import { handleApiError } from '@/utils';

const { mutate, isPending } = useApiMutation(apiClient.users.patchCurrent);

mutate(
  { fullName: 'Ada Lovelace' },
  { onError: (e) => handleApiError(e) },
);
```

Dynamic path params come from the input — there's no separate `pathParams` argument. `apiClient.users.update({ userId, ...changes })` resolves to `PUT /users/{userId}`; the path segment and the body are one typed input.

## Query keys

Keys are derived from the procedure via the `ORPC_PATH` symbol the client attaches to every node. `queryKey(procedure, input)` returns the path array (plus the input when present), so invalidating or seeding a query never means stringly-typed keys.

```tsx theme={null}
import { queryKey } from '@/hooks';
import { apiClient } from '@/services/api-client.service';
import queryClient from '@/query-client';

// key for a list query with input
const key = queryKey(apiClient.users.list, { page: 1 });

// invalidate every users.list query
queryClient.invalidateQueries({ queryKey: queryKey(apiClient.users.list) });

// seed the cache after a mutation
queryClient.setQueryData(queryKey(apiClient.users.getCurrent), updatedUser);
```

`useApiQuery` calls `queryKey` for you; reach for it directly only when you invalidate or write the cache by hand. This is the same primitive the authenticated route uses to prefetch the current user:

```tsx theme={null}
import { createFileRoute, redirect } from '@tanstack/react-router';

import { queryKey } from '@/hooks';
import { apiClient } from '@/services/api-client.service';
import queryClient from '@/query-client';

export const Route = createFileRoute('/_authenticated')({
  beforeLoad: async () => {
    const key = queryKey(apiClient.users.getCurrent);
    if (queryClient.getQueryData(key)) return;

    try {
      await queryClient.fetchQuery({
        queryKey: key,
        queryFn: () => apiClient.users.getCurrent(),
      });
    } catch {
      throw redirect({ to: '/sign-in' });
    }
  },
});
```

## The current user

Reading the signed-in user is common enough to have its own hook, `useCurrentUser`, built on the same pieces — `apiClient.users.getCurrent` and a derived key cached forever:

```tsx theme={null}
import { useCurrentUser } from '@/hooks';

function Greeting() {
  const { data: user } = useCurrentUser();
  return <span>Hi, {user?.fullName}</span>;
}
```

It also subscribes to the `user:updated` socket event and writes fresh data straight into the cache, so the UI stays current without refetching. See [Services](/docs/web/services) for the socket wiring.

## Where types come from

The web app never imports a generated client bundle. The API emits TypeScript declarations (`pnpm --filter api build:types`) and the web app depends on `"api": "workspace:*"`, importing `import type { AppClient } from 'api'`. The contract — `import { contract } from 'api/contract'` — drives the oRPC link's routing. One source of truth, no drift. See [How Ship works](/docs/how-ship-works).
