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

# Typed frontend

> Call your endpoints from a frontend with paths, request types and response types generated from the backend, so a renamed endpoint or column is a compile error.

The frontend never hand-types a URL or a request body. Three things come from the backend:

| What | From | Costs in the bundle |
| - | - | - |
| Paths and verbs | `routePath()` and `ROUTES` in `xano/routes.gen.ts` | about 1 kB of plain data |
| Request types | `RouteInputs` in the same file | nothing, types only |
| Response types | `InferResponse` on an `import type` of the def | nothing, types only |

## Generate the route manifest

```bash theme={null}
npm run xano:routes   # xanosdk routes ./xano/index.ts --emit xano/routes.gen.ts
```

The scaffold's `dev`, `build`, `typecheck` and `xano:deploy` scripts run it first, and
`npm run xano:check` fails when the committed file is stale. Commit `xano/routes.gen.ts`.

A route key is `"<VERB> <name>"`, the endpoint's own identity: `"GET posts/{id}"`,
`"POST posts"`. When two API groups both hold the same verb and name, those endpoints are keyed
`"<group>:<VERB> <name>"` instead.

## Call an endpoint

```ts theme={null}
// frontend/src/lib/api.ts
import type { InferResponse } from "@xano/sdk";
import type { createPost, getPost } from "../../../xano/api/blog.js";
import { ROUTES, routePath, type RouteInputs } from "../../../xano/routes.gen.js";

declare global {
  interface Window {
    XANO_HOST?: string;
  }
}

// The backend URL: injected by a deploy as window.XANO_HOST, else VITE_XANO_HOST in dev.
export const XANO_HOST: string =
  (typeof window !== "undefined" && window.XANO_HOST) ||
  import.meta.env.VITE_XANO_HOST ||
  "";

export type NewPost = RouteInputs["POST posts"];
export type Post = InferResponse<typeof createPost>;
export type MaybePost = InferResponse<typeof getPost>;

export async function fetchPost(id: number): Promise<MaybePost> {
  const res = await fetch(XANO_HOST + routePath("GET posts/{id}", { id }), {
    method: ROUTES["GET posts/{id}"].verb,
  });
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}

export async function createPostRequest(body: NewPost, token: string): Promise<Post> {
  const res = await fetch(XANO_HOST + routePath("POST posts"), {
    method: ROUTES["POST posts"].verb,
    headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}
```

* **`routePath` fills and encodes path params.** Keys and `{param}` names are checked at compile
  time, so a renamed endpoint stops compiling instead of returning a 404.
* **Import defs as types only.** `import type` erases to nothing. Importing a def as a value runs
  its `s.*` and `c.*` calls at load time and pulls the SDK runtime into the bundle (about
  65 kB gzipped).
* **`InferResponse` follows the stack.** A `db.get` response is `Row | null`, a `db.add` is the
  written row, and an object-literal response has those keys. Where the shape cannot be known
  statically, it is `unknown`; declare `responseShape` on the query to fix every caller.
* **`InferRow<typeof table>`** gives a table's row type, for props and state.

## The backend URL

`npm run xano:deploy` writes the Xano Engine's URL into `.env.local` as `VITE_XANO_HOST`;
restart the dev server to pick it up. A deploy with `--static` instead injects the backend URL
into the built HTML as `window.XANO_HOST`, so one build serves any backend with no rebuild. The
`XANO_HOST` constant above reads both. See [Frontend hosting](/run/frontend-hosting).

## Validate at runtime with zod

The types check what you send at compile time. To validate data at runtime too, install the zod
add-on:

```bash theme={null}
npx xanosdk marketplace install zod
```

It adds `ROUTE_SCHEMAS`, `CHANNEL_SCHEMAS` and `MESSAGE_SCHEMAS` to `routes.gen.ts` under the
same keys, each checked against its type when you type-check:

```ts theme={null}
import { ROUTE_SCHEMAS } from "../../../xano/routes.gen.js";

const body = ROUTE_SCHEMAS["POST posts"].parse(untrustedInput);
```

## Realtime

When the workspace has realtime servers, the same file exports `socketUrl()` and
`channelPath()`, plus `ChannelInputs` and `MessageInputs` for their params and payloads:

```ts theme={null}
import { socketUrl, channelPath, type MessageInputs } from "../../../xano/routes.gen.js";

const ws = new WebSocket(socketUrl("chat", XANO_HOST), token); // the token is the subprotocol
const channel = channelPath("rooms/{room_id}", { room_id: 42 });
const payload: MessageInputs["rooms/{room_id} send"] = { body: "hi" };

// The server finishes its handshake first, so wait briefly before the first frame.
ws.onopen = () =>
  setTimeout(() => {
    ws.send(JSON.stringify({ action: "join", channel }));
    ws.send(JSON.stringify({ action: "broadcast", channel, type: "send", payload }));
  }, 500);
```

`socketUrl` turns a tenant base URL into the socket's own address form, which a frontend cannot
build by hand. A listen-only client must send `{ action: "ping" }` every few minutes, or the
idle socket is closed.

## Next

* Every inferred shape and the `getPath`/`toSearchParams` helpers:
  [authoring reference](/reference/authoring).
* Serve the built frontend from Xano: [Frontend hosting](/run/frontend-hosting).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.