---
url: /fetch/type-safety.md
description: >-
  Fetch clients are fully typed - parameters, payload, and response all mirror
  the backend route. Access response types on the client with the ResponseT map.
---

The fetch client mirrors your backend route exactly.
Parameters, payload, and response are all typed from the same route definition -
change the route, and every call site updates with it.

Parameters and payload are checked the moment you call a method: the signature expects the
parameter array and payload shape your route declares, and passing the wrong thing is a
compile error. Nothing to import, nothing to annotate.

```ts [pages/example/index.tsx]
import fetchClients from "_/fetch";

const { GET } = fetchClients["users/[id]"];

await GET([123]);                      // ✅ number param, as declared
await GET(["abc"]);                    // ❌ type error - param must be a number
await GET([123], { query: { q: 1 } }); // ❌ type error - `q` isn't in the query type
```

## Response Types

The response is fully typed at the call site too - awaiting `GET(...)` gives you a value typed
to that route's response, no annotation needed. So most of the time you never name the response
type at all; you just use the result.

What you occasionally need is the response type *out of band* - away from the call,
where there's no awaited result to infer from. Typing a Solid `createAsync` accessor,
a `useLoaderData()` result, a store, a component prop, or a shared helper are the common cases.

For those, KosmoJS derives a `ResponseT` map, exported from `_/fetch`.

`ResponseT` is keyed by route name, then request method, and resolves to the response **body**
type declared on that handler - the same type awaiting the method gives you, available as a
name you can reference anywhere.

Given this backend route:

```ts [api/users/[id]/index.ts]
export default defineRoute<"users/[id]", [number]>(({ GET }) => [
  GET<{
    query: { name?: string },
    response: [200, "json", { id: number; name: string; email: string }],
  }>(async (ctx) => { /* ... */ }),
]);
```

the response type is available on the client as:

```ts [pages/example/index.tsx]
import f, { type ResponseT } from "_/fetch";

ResponseT["users/[id]"]["GET"]; // { id: number; name: string; email: string }
```

::: tip Response types are opt-in
An entry exists in `ResponseT` only when the handler declares a `response`.
If a route defines no response type, it has no `ResponseT` entry -
just as it has no response validation. Declare a `response` to get both.
:::

### Without a `response`, the result is `unknown`

This is worth being explicit about, because it is the one case where the client is *not* typed for you.
A method whose handler declares no `response` returns `Promise<unknown>`:

```ts
// api/users/index.ts - no `response` declared
export default defineRoute<"users">(({ GET }) => [
  GET<{
    query: { page?: number },
    // no response schema // [!code hl]
  }>(async (ctx) => {
    // ...
  }),
]);
```

```ts
const users = await fetchClients["users"].GET();
users.length;  // ❌ 'users' is of type 'unknown'
```

`unknown` is deliberate rather than `any`: nothing was declared, so nothing is claimed,
and TypeScript makes you say what you expect instead of silently trusting it.

Declaring `response` on the handler is what fixes it - and it is the same one line that
turns on response validation and the OpenAPI response schema:

```ts
GET<{
  response: [200, "json", Array<User>] // [!code hl]
}>(async (ctx) => { /* ... */ })
```

```ts
const users = await fetchClients["users"].GET();
users.length;  // ✅ Array<User>
```

So the practical rule: **declare `response` on any handler whose result the frontend consumes.**
Params and payload are typed from the route definition regardless - it is only the return value that depends on `response`.

## Multiple Responses

A handler can declare a union of responses - different status codes returning different bodies.
`ResponseT` follows, collapsing to a union of the body types:

```ts [api/users/index.ts]
export default defineRoute<"users">(({ POST }) => [
  POST<{
    json: NewUser,
    response:
      | [201, "json", User]             // created - full record
      | [202, "json", { queued: true }] // accepted for async processing
      | [409]                           // conflict, no body
  }>(async (ctx) => {
    // response must comply with one of the defined schemas
  }),
]);
```

Variants without a body - no third tuple element, like the bare `[409]` above -
contribute nothing to the type, so they drop out of the union:

```ts [pages/example/index.tsx]
import f, { type ResponseT } from "_/fetch";

ResponseT["users"]["POST"]; // User | { queued: true }
```

## Using Response Types

The out-of-band cases in practice - the hook that needs annotating at its untyped boundary -
and a shared helper naming the type once. `helpers.ts` is the same file for every framework.

### Type Safety | Using Response Types | React

```tsx
// React: pages/users/[id]/index.tsx
import { useLoaderData } from "react-router";
import f from "_/fetch";
import { formatUser, type User } from "./helpers";

const { GET } = f["users/[id]"];

export const loader = ({ params }) => GET([params.id]);

export default function UserProfile() {
  // useLoaderData is untyped at the boundary - annotate it with User type
  const user = useLoaderData<User>();
  return <div>{formatUser(user)}</div>;
}
```

### Type Safety | Using Response Types | Solid

```tsx
// Solid: pages/users/[id]/index.tsx
import { Show, Suspense } from "solid-js";
import { createAsync, query, useParams } from "@solidjs/router";
import f from "_/fetch";
import { formatUser, type User } from "./helpers";

const { GET } = f["users/[id]"];

// createAsync infers from the fetcher, so there is no boundary to annotate -
// User earns its keep on the query wrapper and on helper signatures
const getUser = query((id: string): Promise<User> => GET([id]), "user");

export const preload = ({ params }) => getUser(params.id);

export default function UserProfile() {
  const params = useParams();
  const user = createAsync(() => getUser(params.id));
  return (
    <Suspense>
      <Show when={user()}>{(u) => <div>{formatUser(u())}</div>}</Show>
    </Suspense>
  );
}
```

### Type Safety | Using Response Types | Vue

```vue
<!-- Vue: pages/users/[id]/index.vue -->
<script lang="ts">
import f from "_/fetch";

const { GET } = f["users/[id]"];

export const loader = ({ params }) => GET([params.id]);
</script>

<script setup lang="ts">
import { useLoaderData } from "_/use";
import { formatUser, type User } from "./helpers";

// useLoaderData takes the type argument - it returns User | undefined
const user = useLoaderData<User>();
</script>

<template>
  <div v-if="user">{{ formatUser(user) }}</div>
</template>
```

### Type Safety | Using Response Types | Svelte

```svelte
<!-- Svelte: pages/users/[id]/index.svelte -->
<script module lang="ts">
import f from "_/fetch";

const { GET } = f["users/[id]"];

export const loader = ({ params }) => GET([params.id]);
</script>

<script lang="ts">
import { useLoaderData } from "_/use";
import { formatUser, type User } from "./helpers";

// useLoaderData takes the type argument - it returns User | undefined
const user = useLoaderData<User>();
</script>

{#if user}
  <div>{formatUser(user)}</div>
{/if}
```

### Type Safety | Using Response Types | ./helpers.ts

```ts
// helpers.ts - framework-agnostic, identical in every folder
import type { ResponseT } from "_/fetch";

// name the type once, reuse it across components
export type User = ResponseT["users/[id]"]["GET"];

export const formatUser = (user: User) => {
  return `${user.name} [${user.email}]`;
}
```

> MDX is absent here on purpose: `.mdx` is not TypeScript,
> so it has no type arguments and no type imports - an MDX page reads `useLoaderData()` untyped.

The type flows from the backend route through the client to wherever you consume it.
Change the route's `response` shape and every consumer - loaders, resources, helpers, props -
updates with it, surfacing mismatches at compile time instead of in production.

See [Fetch Client Integration](/fetch/integration.md) for the full set of framework patterns,
and [Response Validation](/validation/response.md) for declaring the `response` schema on the backend.
