# Type Safety

How Courier achieves end-to-end type safety from API definitions to components — types inferred, never generated.

## Overview

Courier provides full type safety across the entire request lifecycle: from your API definition to the data in your components. Types are inferred, never generated: no schema files, no step to run before you compile. TypeScript functions are the single source of truth.

## The Builder Chain

Type safety starts with the chain. Each step that changes the endpoint's types returns a builder carrying them forward:

- `.response()` — what the backend returns
- `.variables()` — what the caller must pass. Absent, the endpoint takes no arguments.

Both take a schema, or a type argument when the API publishes no schema worth validating against.

```ts title="api/users.ts"
import { courier, defineGroup } from "@noverax/courier/define"

const { endpoint } = courier()

type User = { id: string; name: string; email: string }
type UserList = { data: User[]; total: number }

export const users = defineGroup({
  prefix: "users",
  endpoints: {
    // Response only — the caller passes nothing
    getAll: endpoint.get("").response<UserList>(),

    // Variables the caller must pass, and the response they get back
    getById: endpoint.get(":id").variables<{ id: string }>().response<User>(),

    // A write addresses the same wire through the mutation head
    create: endpoint.post("").variables<{ name: string; email: string }>().response<User>(),
  },
})
```

## The Inference Chain

Courier uses TypeScript's type inference to propagate types through dot-notation access:

```typescript
// 1. You define the API
export const api = defineApi({ baseURL: "...", groups: { users } })

// 2. You create a typed query caller, from the routes Courier writes from it
const q = createProxyQueryCaller(apiRoutes)

// 3. TypeScript infers everything at the call site
const { data: list } = q.users.useQuery("getAll")
//            ^? UserList — inferred from .response<UserList>()

const { data: user } = q.users.useQuery("getById", {
  variables: { id: "5" },
  //           ^? { id: string } — inferred from .variables<{ id: string }>()
})

const { mutate } = q.users.useMutation("create")
mutate({ name: "Jane", email: "jane@test.com" })
//      ^? { name: string; email: string } — inferred from the chain
```

> **No manual type annotations**
>
> You never need to write explicit response types. The response type is inferred automatically. Types stay in one place and are never duplicated.

## Dot-Notation Validation

Group and method names are validated at the type level. TypeScript gives you autocomplete and compile-time errors:

```typescript
// Group names are validated
caller.users.getAll()       // valid
caller.unknown.getAll()     // Type error: "unknown" is not a valid group

// Method names are validated
caller.users.getAll()       // valid
caller.users.unknown()      // Type error: "unknown" is not a method on users
```

## Variables Enforcement

TypeScript enforces whether variables are required, optional, or forbidden:

```typescript
// No variables — V defaults to undefined
endpoint.get("").response<User[]>()

caller.users.getAll()                          // valid
caller.users.getAll({ page: 1 })               // Type error — no variables expected

// Required variables
endpoint.get(":id").variables<{ id: string }>().response<User>()

caller.users.getById()                         // Type error — variables required
caller.users.getById({ id: 123 })              // Type error — id must be string
caller.users.getById({ id: "5" })              // valid
```

## Pattern: Well-Typed Endpoints

For the best DX, define domain types explicitly and use utility types:

```ts title="api/products.ts"
import { courier, defineGroup } from "@noverax/courier/define"

const { endpoint } = courier()

type Product = { id: string; name: string; price: number; category: string }

type ProductFilters = {
  category?: string
  minPrice?: number
  maxPrice?: number
  page?: number
}

type CreateProductVars = Omit<Product, "id">
type UpdateProductVars = { id: string } & Partial<CreateProductVars>

export const products = defineGroup({
  prefix: "products",
  endpoints: {
    list: endpoint.get("").variables<ProductFilters>().response<Product[]>(),
    getById: endpoint.get(":id").variables<{ id: string }>().response<Product>(),
    create: endpoint.post("").variables<CreateProductVars>().response<Product>(),
    update: endpoint.patch(":id").variables<UpdateProductVars>().response<Product>(),
    remove: endpoint.del(":id").variables<{ id: string }>().response<null>(),
  },
})

// Everything flows automatically:
// q.products.useQuery("list", { variables: { category: "shoes" } })
//                                           ^? ProductFilters
// → data is Product[]
```

## Schema-Inferred Types

When using runtime validation, types can be inferred from schemas — no manual generics needed:

```typescript
import { z } from "zod"

const UserSchema = z.object({ id: z.string(), name: z.string(), email: z.string() })

// Types inferred from the schema — no type argument needed
endpoint.get("").response(z.array(UserSchema))
// → response type is { id: string; name: string; email: string }[]
```

## Next Steps

- [Defining APIs](/docs/core-concepts/defining-apis) — defineApi, defineGroup, endpoint
- [Error Handling](/docs/core-concepts/error-handling) — typed error data with HTTPError
- [Validation](/docs/validation/standard-schema) — runtime schema validation
