# Server state: AtomRpc & AtomHttpApi

**Unstable:** Both modules live under `effect/unstable/reactivity`. The `unstable` namespace
ships in core `effect`, but its API may change between minor releases.

`AtomRpc` and `AtomHttpApi` turn **remote endpoints into atoms**. Instead of
calling a client by hand and threading the result into your own state, you
describe a service once and get back:

- a generated, typed **client** (the `Context.Service` shape),
- an **`AtomRuntime`** that provides the transport to every atom it creates,
- **`query(...)`** — reads become an `Atom` of [`AsyncResult`](https://effect.plants.sh/reactivity/async-result/)
  (or a writable [pull atom](https://effect.plants.sh/reactivity/atom/) for streaming RPCs), and
- **`mutation(...)`** — writes become an
  [`AtomResultFn`](https://effect.plants.sh/reactivity/atom/): write the request, observe the result.

Both modules speak the same reactivity vocabulary: queries take
`reactivityKeys` so they refresh when those keys are invalidated, mutations
invalidate the same keys after they succeed (see
[Reactivity & invalidation](https://effect.plants.sh/reactivity/invalidation/)), and non-streaming
results can carry a `serializationKey` for SSR hydration (see
[AtomRef & hydration](https://effect.plants.sh/reactivity/atom-ref/)).

The two modules differ only in transport:

| Module        | Source of truth                | Read returns                                   | Write returns  |
| ------------- | ------------------------------ | ---------------------------------------------- | -------------- |
| `AtomRpc`     | an `RpcGroup` (RPCs)           | `Atom<AsyncResult>` — or a pull atom if streaming | `AtomResultFn` |
| `AtomHttpApi` | an `HttpApi` (HTTP endpoints)  | `Atom<AsyncResult>`                            | `AtomResultFn` |

---

## Part 1 — AtomRpc

### The common case

Define an `RpcGroup`, then extend `AtomRpc.Service<Self>()` with the group and a
client `protocol` layer. The resulting class is both the client service *and*
the carrier of `query` / `mutation` atoms.

```ts
import { Effect, Layer, Schema } from "effect"
import { AtomRpc } from "effect/unstable/reactivity"
import { Rpc, RpcClient, RpcGroup } from "effect/unstable/rpc"
import { RpcSerialization } from "effect/unstable/rpc"
import { FetchHttpClient } from "effect/unstable/http"

// 1. Describe the RPCs (payload + success schemas).
const GetTodos = Rpc.make("GetTodos", {
  success: Schema.Array(Schema.Struct({ id: Schema.Number, text: Schema.String }))
})

const CreateTodo = Rpc.make("CreateTodo", {
  payload: { text: Schema.String },
  success: Schema.Struct({ id: Schema.Number, text: Schema.String })
})

const TodosRpc = RpcGroup.make(GetTodos, CreateTodo)

// 2. Bind the group to an atom runtime via a transport `protocol` layer.
class Todos extends AtomRpc.Service<Todos>()("app/Todos", {
  group: TodosRpc,
  protocol: Layer.provide(
    RpcClient.layerProtocolHttp({ url: "/api/rpc" }),
    [RpcSerialization.layerJson, FetchHttpClient.layer]
  )
}) {}

// 3. Reads -> an Atom of AsyncResult.
const todosAtom = Todos.query("GetTodos", undefined, {
  reactivityKeys: ["todos"]
})
// => Atom<AsyncResult<ReadonlyArray<{ id, text }>, RpcClientError>>

// 4. Writes -> an AtomResultFn. Invalidating "todos" reruns todosAtom.
const createTodo = Todos.mutation("CreateTodo")
// write { payload: { text: "..." }, reactivityKeys: ["todos"] } to run it
```

`Todos` is itself a `Context.Service` whose value is the **flattened**
`RpcClient`, so you can also call the client directly inside Effect code when
you do not need an atom:

```ts
const program = Effect.gen(function* () {
  const client = yield* Todos
  return yield* client("GetTodos", undefined)
})
```
**Note:** The `protocol` layer can be **static** (as above) or a **function of the atom
context** — `(get) => Layer.Layer<...>` — when the transport depends on other
atoms (for example an auth token atom that becomes a request header). The same
`get` form is available for `AtomHttpApi`'s `httpClient`.

### Streaming RPCs become pull atoms

If an RPC's success schema is a stream (`Rpc.make(..., { stream: true })`),
`query` returns a **writable pull atom** instead of an `Atom<AsyncResult>`.
Write `void` to it to advance the stream; it is not serializable.

```ts
import { Schema } from "effect"
import { Rpc, RpcGroup } from "effect/unstable/rpc"

const Events = Rpc.make("Events", {
  success: Schema.String,
  error: Schema.Never,
  stream: true
})

const StreamGroup = RpcGroup.make(Events)
// Streams.query("Events", undefined)
// => Writable<PullResult<string, RpcClientError>, void>
```

### AtomRpc reference

#### `AtomRpc.Service<Self>()(id, options)`

Creates the `Context.Service` class. `<Self>()` is the usual two-step
self-reference; the second call takes a unique string `id` and the options
below.

```ts
class Todos extends AtomRpc.Service<Todos>()("app/Todos", {
  group: TodosRpc,
  protocol: myProtocolLayer
}) {}
// => class Todos (a Context.Service) with .runtime, .query, .mutation
```

The full options object:

- **`group`** (`RpcGroup<Rpcs>`, required) — the RPC group to expose.
- **`protocol`** (required) — the client transport `Layer`, or
  `(get: AtomContext) => Layer`. Provide a serialization layer
  (`RpcSerialization.layerJson`, ...) and an `HttpClient`/socket/worker layer
  to it.
- **`spanPrefix`** (`string`) — prefix for client request spans.
- **`spanAttributes`** (`Record<string, unknown>`) — extra attributes added to
  request spans.
- **`generateRequestId`** (`() => RequestId`) — override request id generation.
- **`disableTracing`** (`boolean`) — turn off client tracing.
- **`makeEffect`** — supply a custom `Effect` producing the flattened client
  instead of letting the service build one from `group` + `protocol`.
- **`runtime`** (`Atom.RuntimeFactory`) — use a non-default runtime factory
  (e.g. one with shared global layers / `MemoMap`).

```ts
class Todos extends AtomRpc.Service<Todos>()("app/Todos", {
  group: TodosRpc,
  protocol: myProtocolLayer,
  spanPrefix: "TodosClient",
  disableTracing: false
}) {}
```

#### `.query(tag, payload, options?)`

Builds (and memoizes) an atom for a single RPC read. Non-streaming RPCs return
`Atom<AsyncResult<Success, Error | RpcClientError | MiddlewareError>>`;
streaming RPCs return a writable pull atom. The atom's identity is keyed on the
payload, normalized headers, reactivity keys, TTL, and serialization key — so
keep those stable when atom identity matters.

```ts
const byId = Todos.query("GetTodos", undefined)
// => Atom<AsyncResult<ReadonlyArray<...>, RpcClientError>>
```

`options`:

- **`headers`** (`Headers.Input`) — per-request headers; part of the cache key.
- **`reactivityKeys`** (`ReadonlyArray<unknown>` or
  `Record<string, ReadonlyArray<unknown>>`) — refresh this atom whenever any of
  these keys is invalidated.
- **`timeToLive`** (`Duration.Input`) — idle TTL. A finite duration sets the
  atom's idle TTL; an infinite duration keeps it alive.
- **`serializationKey`** (`string`) — marks the (non-streaming) atom
  serializable for hydration. Codecs are derived from the RPC success schema and
  the combined RPC + middleware + `RpcClientError` schemas. Choose stable,
  unique keys.

```ts
const todos = Todos.query("GetTodos", undefined, {
  reactivityKeys: ["todos"],
  timeToLive: "30 seconds",
  serializationKey: "todos:all"
})
```

#### `.mutation(tag)`

Builds an `AtomResultFn` for a single RPC write. Calling it with a `tag` is
memoized, so the same tag yields the same atom. Streaming RPCs are rejected at
the type level (`never`). Mutations are always serializable in decoded form
(key `AtomRpc:mutation:<tag>`).

The write argument is an object:

- **`payload`** — the RPC payload (constructed from the RPC's payload schema).
- **`reactivityKeys`** (array or record) — invalidated **after** the mutation
  succeeds, rerunning any query registered on those keys.
- **`headers`** (`Headers.Input`) — per-request headers.

```ts
const create = Todos.mutation("CreateTodo")
// write to it (via your UI binding / registry):
//   create -> { payload: { text: "Buy milk" }, reactivityKeys: ["todos"] }
// => AtomResultFn whose result is AsyncResult<{ id, text }, RpcClientError | ...>
```

#### `.runtime`

The `Atom.AtomRuntime` backing this service. Use it to derive additional atoms
that need the same client/transport in scope, or to mount the runtime layer.

```ts
import { Atom } from "effect/unstable/reactivity"

// A derived read that uses the client directly through the runtime.
const firstTodo = Todos.runtime.atom(
  Todos.use((client) => client("GetTodos", undefined))
)
// => Atom<AsyncResult<ReadonlyArray<...>, ...>>
```

#### `AtomRpc.AtomRpcClient<Self, Id, Rpcs>`

The interface describing the generated service: it extends the flattened
`RpcClient` `Context.Service` and adds `runtime`, `query`, and `mutation`. You
rarely name it directly — `class X extends AtomRpc.Service<X>()(...)` produces a
value of this type.

---

## Part 2 — AtomHttpApi

### The common case

`AtomHttpApi` wraps `HttpApiClient.make`, so you supply an `HttpApi` definition,
an `HttpClient` layer, and (optionally) a `baseUrl`. Every endpoint becomes
available as a method on the generated client, and as `query` / `mutation`
atoms.

```ts
import { Schema } from "effect"
import { AtomHttpApi } from "effect/unstable/reactivity"
import { FetchHttpClient } from "effect/unstable/http"
import {
  HttpApi,
  HttpApiEndpoint,
  HttpApiGroup
} from "effect/unstable/httpapi"

// 1. Describe the API. In v4, success/payload schemas are passed in the
// endpoint options object (not via builder methods).
const Todo = Schema.Struct({ id: Schema.Number, text: Schema.String })

const TodosGroup = HttpApiGroup.make("todos")
  .add(
    HttpApiEndpoint.get("list", "/todos", {
      success: Schema.Array(Todo)
    })
  )
  .add(
    HttpApiEndpoint.post("create", "/todos", {
      payload: { text: Schema.String },
      success: Todo
    })
  )

const Api = HttpApi.make("api").add(TodosGroup)

// 2. Bind it to an atom runtime.
class TodosApi extends AtomHttpApi.Service<TodosApi>()("app/TodosApi", {
  api: Api,
  httpClient: FetchHttpClient.layer,
  baseUrl: "/api"
}) {}

// 3. Reads -> an Atom of AsyncResult (decoded by default).
const listAtom = TodosApi.query("todos", "list", {
  reactivityKeys: ["todos"]
})
// => Atom<AsyncResult<ReadonlyArray<Todo>, ...endpoint + middleware errors...>>

// 4. Writes -> an AtomResultFn that invalidates "todos" on success.
const createTodo = TodosApi.mutation("todos", "create")
// write { payload: { text: "..." }, reactivityKeys: ["todos"] } to run it
```
**What surfaces as a defect vs a typed error:** Endpoint and middleware failures stay **typed errors** in the `AsyncResult`.
Schema decode/encode failures (`SchemaError`) and low-level HTTP client failures
(`HttpClientError`) are intentionally re-raised as **defects** — they indicate a
contract or transport bug, not a domain error, so they do not pollute the typed
error channel.

### Response modes

Endpoints support a `responseMode` controlling what the atom resolves to:

- `"decoded-only"` (default) — the decoded success value. Required for
  serialization.
- `"decoded-and-response"` — `[Success, HttpClientResponse]`.
- `"response-only"` — the raw `HttpClientResponse` (no decoded success, and no
  typed endpoint error).

```ts
const withResponse = TodosApi.query("todos", "list", {
  responseMode: "decoded-and-response"
})
// => Atom<AsyncResult<[ReadonlyArray<Todo>, HttpClientResponse], ...>>
```

Only `"decoded-only"` atoms are serializable — avoid serializing modes that
expose a raw `HttpClientResponse`.

### AtomHttpApi reference

#### `AtomHttpApi.Service<Self>()(id, options)`

Creates the `Context.Service` class around `HttpApiClient.make`. The same
`HttpApi`, schemas, base URL, middleware services, and HTTP client layer must be
available wherever the runtime is built.

```ts
class TodosApi extends AtomHttpApi.Service<TodosApi>()("app/TodosApi", {
  api: Api,
  httpClient: FetchHttpClient.layer
}) {}
// => class TodosApi with .runtime, .query, .mutation
```

Options:

- **`api`** (`HttpApi<Id, Groups>`, required) — the API definition.
- **`httpClient`** (required) — a `Layer` providing the API's client services
  and an `HttpClient`, or `(get: AtomContext) => Layer` to derive it from atom
  context.
- **`baseUrl`** (`URL | string`) — base URL prepended to endpoint paths.
- **`transformClient`** (`(client) => client`) — cross-cutting `HttpClient`
  transform (e.g. attach default headers, retries, logging).
- **`transformResponse`** (`(effect) => effect`) — wrap every endpoint effect,
  e.g. for instrumentation, before decoding.
- **`runtime`** (`Atom.RuntimeFactory`) — use a non-default runtime factory.

```ts
class TodosApi extends AtomHttpApi.Service<TodosApi>()("app/TodosApi", {
  api: Api,
  httpClient: FetchHttpClient.layer,
  baseUrl: "https://api.example.com",
  transformClient: (client) => client // e.g. .pipe(HttpClient.retryTransient(...))
}) {}
```

#### `.query(group, endpoint, request)`

Builds (and memoizes) an atom for an endpoint read. `request` carries the
endpoint inputs (`params`, `query`, `payload`, `headers`) plus the reactivity
controls. The atom resolves to
`AsyncResult<Response, EndpointError | MiddlewareError>`.

```ts
const one = TodosApi.query("todos", "list", {})
// => Atom<AsyncResult<ReadonlyArray<Todo>, ...>>
```

`request` extras:

- **`reactivityKeys`** — refresh on key invalidation.
- **`timeToLive`** (`Duration.Input`) — finite -> idle TTL, infinite -> keep
  alive.
- **`serializationKey`** (`string`) — serializable in `"decoded-only"` mode
  only; codecs derive from the endpoint's success/error schemas. Choose keys
  that uniquely identify the request.
- **`responseMode`** — as described above (defaults to `"decoded-only"`).

```ts
const list = TodosApi.query("todos", "list", {
  reactivityKeys: ["todos"],
  timeToLive: "1 minute",
  serializationKey: "todos:list"
})
```

#### `.mutation(group, endpoint, options?)`

Builds an `AtomResultFn` for an endpoint write. The write argument is the
endpoint's decoded `ClientRequest` (`params` / `query` / `payload` / `headers`)
plus `reactivityKeys` invalidated on success. `options.responseMode` defaults to
`"decoded-only"`, which also makes the mutation serializable (key
`AtomHttpApi:mutation:<group>:<endpoint>`).

```ts
const create = TodosApi.mutation("todos", "create")
// write { payload: { text: "Write docs" }, reactivityKeys: ["todos"] }
// => AtomResultFn whose result is AsyncResult<Todo, ...>

// Opt into the raw response instead:
const createRaw = TodosApi.mutation("todos", "create", {
  responseMode: "response-only"
})
```

#### `.runtime`

The `Atom.AtomRuntime` backing the service — identical role to `AtomRpc`'s.
Use it to build extra atoms that need the generated client in scope.

```ts
import { Atom } from "effect/unstable/reactivity"

const count = TodosApi.runtime.atom(
  TodosApi.use((client) => client.todos.list({}))
)
// => Atom<AsyncResult<ReadonlyArray<Todo>, ...>>
```

#### `AtomHttpApi.AtomHttpApiClient<Self, Id, Groups>`

The interface for the generated service: it extends the
`HttpApiClient.Client<Groups>` `Context.Service` and adds `runtime`, `query`,
and `mutation`. Produced for you by `class X extends AtomHttpApi.Service<X>()(...)`.

---

## Hydration, reactivity, and TTL in one place

The three features that make these modules "server state" rather than plain
clients all flow through the underlying [`Atom`](https://effect.plants.sh/reactivity/atom/) machinery,
so they behave identically across both modules:

- **`reactivityKeys`** — on a query they register the atom with the
  [`Reactivity`](https://effect.plants.sh/reactivity/invalidation/) service so it refreshes when a key
  is invalidated; on a mutation they are invalidated after success. Keep keys
  stable across the client and server registries.
- **`timeToLive`** — a finite `Duration` becomes the atom's idle TTL
  (`Atom.setIdleTTL`); an infinite one keeps it alive (`Atom.keepAlive`).
- **`serializationKey`** (queries) / decoded-only mode (mutations) — wrap the
  atom with `Atom.serializable`, deriving codecs from the endpoint/RPC schemas.
  This is what lets a server-rendered result be dehydrated and rehydrated on the
  client; see [AtomRef & hydration](https://effect.plants.sh/reactivity/atom-ref/) for moving those
  serialized values across the boundary, and the serializable combinators on the
  [Atom](https://effect.plants.sh/reactivity/atom/) page for the lower-level primitive.

```ts
// A fully-configured server-state read: refreshes on "todos",
// expires after 30s idle, and round-trips through SSR hydration.
const todos = TodosApi.query("todos", "list", {
  reactivityKeys: ["todos"],
  timeToLive: "30 seconds",
  serializationKey: "todos:list"
})
```