# Mental model

> Follow a request from the typed client to its server handler and back.

## Useful when

- You are deciding what belongs in shared, client, server, or host code.
- You need to choose a transport or understand the request lifecycle.

Define a route once in a shared contract. The server adds a handler for it, and the client turns it into a typed function you can call.

A [transport](https://hulla.dev/docs/api/transports.md) connects the client to the server. Use HTTP for browser requests or another service, and a direct call when both run in the same process.

### @hulla/api architecture

1. **Typed client** — Builds a call from the shared contract and narrows the returned status.
2. **Transport** — Carries one invocation over Fetch, in process, MessagePort, or WebSocket.
3. **Adapter** — Connects the portable call to host routing, context, request reads, and response writes.
4. **Implementation** — Runs server middleware and the exhaustive route handler.

## Put each decision in one place

| File                            | Runtime     | Put this here                                                   |
| ------------------------------- | ----------- | --------------------------------------------------------------- |
| `src/api/contract.ts`           | Shared      | Paths, inputs, statuses, bodies, errors, schemas, and codecs    |
| `src/api/implementation.ts`     | Server only | Handlers, services, authorization, and server middleware        |
| `src/api/client.ts`             | Caller      | `createClient()`, client middleware, and the selected transport |
| Framework route or server entry | Host only   | Adapter mounting and native request context                     |

Share the contract with frontend code or publish it as a package. Keep the implementation server-only: it can access databases, secrets, queues, and framework state.

## Follow one call

### Request lifecycle

1. **Select a route** — The client knows the method, path, accepted fields, and possible responses.
2. **Prepare the call** — Client middleware adds auth headers, tracing, or cancellation.
3. **Cross the boundary** — The selected transport carries the call. The adapter connects it to host routing and context.
4. **Validate and handle** — The server decodes request fields, runs middleware, and returns a declared response or error.
5. **Read a typed result** — Check the response status to narrow the body type.

```ts filename="src/features/projects.ts"
const result = await api.projects.byId({ params: { id } })

if (result.status === 200) {
  renderProject(result.body)
} else {
  showNotFound(result.body.message)
}
```

There is no generated client and no hidden global router. The client is a typed view of the contract plus a transport.

## Choose the middle boundary

| Your application needs…                            | Choose                                                                                                                                  | What stays in your application                                          |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Calls inside a trusted server runtime              | [In-process](https://hulla.dev/docs/api/transports/in-process.md)                                                                       | Server rendering, jobs, and tests; validation and middleware still run. |
| HTTP between browsers or services                  | [Fetch](https://hulla.dev/docs/api/transports/fetch.md)                                                                                 | Origin and deployment; the adapter matches Web requests.                |
| An existing framework router                       | A [native adapter](https://hulla.dev/docs/api/servers.md)                                                                               | Routing, hooks, parsed bodies, response writing, and request context.   |
| A worker, desktop bridge, or persistent connection | [MessagePort](https://hulla.dev/docs/api/transports/message-port.md) or [WebSocket](https://hulla.dev/docs/api/transports/websocket.md) | The port or socket lifecycle. Client calls keep the same shape.         |

**Keep in-process clients on the server.** They import the server implementation and must never enter browser code.

## Validation follows direction

A request schema runs on the server after the transport decodes the request. A response schema runs on the server before the adapter sends the response.

The client does not rerun those ordinary schemas. If both sides should work with a richer value than the wire can carry—`Date` in memory and an ISO string in transit—use a codec with explicit encode and decode directions.

Type-only declarations skip application-shape validation altogether. They still use the selected representation, so malformed JSON remains malformed JSON; TypeScript alone does not make untrusted data safe.

## Four rules prevent most mistakes

- Keep framework imports, database clients, and secrets out of the contract module.
- Use ordinary functions when no contract boundary is needed.
- Treat declared non-`2xx` responses as expected values; reserve thrown failures for transport, schema, and unexpected runtime problems.
- Select the smallest contract subtree a client or server module needs.

From here, learn how to [author contracts](https://hulla.dev/docs/api/core/contracts.md), [implement servers](https://hulla.dev/docs/api/core/servers.md), or [build typed clients](https://hulla.dev/docs/api/core/clients.md). If you already know your boundary, jump directly to [transports](https://hulla.dev/docs/api/transports.md) or [server adapters](https://hulla.dev/docs/api/servers.md).
