---
title: React and generated proxies
editUrl: https://github.com/Cratis/Arc.Elixir/edit/main/Documentation/react-proxies.md
description: Opt in to generating typed TypeScript commands and queries for the Arc React packages from your Elixir modules, with the same validation rules as the server.
lastUpdated: 2026-11-05T00:00:00.000Z
---


If you build a React frontend, you want its calls to your backend typed, validated and in step with the server. Arc generates that client from the modules you already wrote. It is opt-in: a backend that serves LiveView, or an API for another client, never generates TypeScript.

The generated files target the existing `@cratis/arc`, `@cratis/arc.react` and `@cratis/fundamentals` packages and follow the C# proxy generator's templates, so those packages work unchanged.

## Turn it on

Add the `:arc` compiler and say where to write. Nothing is generated without both.

```elixir
# From: Samples/phoenix/mix.exs
          compilers: Mix.compilers() ++ [:arc],
          arc: [application: Library.Arc, output: "../frontend/src/arc"],
```

Now every `mix compile` regenerates the proxies, and `mix phx.server` keeps them current as you edit, because Phoenix's code reloader runs the project compilers. You can also run it directly:

```shell
mix arc.gen.proxies
mix arc.gen.proxies --application MyApp.Arc --output assets/src/arc
mix arc.gen.proxies --check          # write nothing; fail if the files are out of date
```

Use `--check` in CI when you commit the generated files. To keep a feature's proxies beside its Elixir and its React page, set `layout: :colocated`: see [Vertical slices](/arc/backend/elixir/vertical-slices/). Add `remove_stale: false` to the `arc:` options (or `--no-remove-stale`) to keep generated files for artifacts that no longer exist.

## What you get

Each artifact becomes one file, in a folder that mirrors its namespace after `segments_to_skip`:

| You wrote | You get |
| --- | --- |
| `MyApp.Authors.Registration.RegisterAuthor` | `Authors/Registration/RegisterAuthor.ts`: an `IRegisterAuthor` interface, a `Command` class with route, roles and property descriptors, a validator, and a `use` hook |
| a `@query` in `MyApp.Authors.Listing.Authors` | `Authors/Listing/AllAuthors.ts`: a `QueryFor` class with parameters, sorting helpers and `use`, `useWithPaging`, `useSuspense`, `useSuspenseWithPaging` and `when` |
| an `@observe` query | the same as an `ObservableQueryFor`, plus `useChangeStream` for a list |
| the read model's struct and nested schemas | a class with `@field` decorators |
| the identity provider's `details:` type | a class to give `useIdentity` or `IdentityProvider.getCurrent`, so `identity.details` is typed |
| each folder | an `index.ts` re-exporting the files in it |

The React app uses them directly:

```tsx
// From: Samples/frontend/src/App.tsx
    // A live query: the server pushes a new list whenever Chronicle changes an author.
    const [authors] = LiveAuthors.use();

    // A command with client-side validation generated from the same rules the server enforces.
    const [register, setRegisterValues] = RegisterAuthor.use({ id: Guid.create(), name: '' });
```

Declared rules (`required`, `min_length`, `max_length`, `email`, `matches` and the number rules) become a validator, so the browser rejects what the server would. A command's `@moduledoc` and a field's `doc:` become the TypeScript documentation.

### Identity

The type named by `use Arc.Identity.Provider, details: …` is generated like any other, and the React app passes it to `useIdentity`:

```tsx
// From: Samples/frontend/src/App.tsx
    // Who is signed in, from GET /.cratis/me. The details are typed from the backend's declared type.
    const identity = useIdentity<IdentityDetails>(IdentityDetails);
```

`identity.details` is then an `IdentityDetails` instance with `displayName` and `canRename`, hydrated from the response.

### Type mapping

| Elixir | TypeScript |
| --- | --- |
| `:string` | `string` |
| `:integer`, `:float` | `number` |
| `:boolean` | `boolean` |
| `:uuid` | `Guid` from `@cratis/fundamentals` |
| `:date` | `DateOnly` from `@cratis/fundamentals` |
| `:datetime` | `Date` |
| `:map` | `object` |
| `{:list, t}` | `t[]` |
| `Chronicle.Concept` | the primitive it wraps |
| a struct or `Arc.Schema` module | a generated class |

A read model's fields come from its struct defaults, and a `nil` or `""` default reads as a string. Override with `use Arc.ReadModel, types: [id: :uuid]`, or declare the model with `Arc.Schema`.

## Safety

Every file starts with `// @generated by Cratis. Source: <module>. Hash: <sha256>`.

- A file without that marker is never overwritten. Generation fails with a message naming it.
- Stale cleanup deletes only files that carry the marker, and removes folders it leaves empty.
- A hand-written `index.ts` is merged, never replaced: missing exports are appended.
- Output is deterministic. Generating twice produces identical bytes and an unchanged file is not rewritten, so watchers and `git diff` stay quiet.
- Two artifacts that would generate the same file are an error that names both.

The generated TypeScript uses inline `type` modifiers for type-only imports, so it compiles under `verbatimModuleSyntax`.

## Proving the contract

The repository's `ContractTests/run.sh` generates proxies for fixtures, compiles them with strict TypeScript against the published packages, then runs the real client against an Arc server: commands, validation, authorization, queries, paging, sorting, the `QUERY` method, the WebSocket hub and Server-Sent Events. See [Build and release](/arc/backend/elixir/contributing/build-and-release/).

## Point Vite at Phoenix

In development, proxy the API and the Arc paths to Phoenix so the browser sees one origin:

```ts
// From: Samples/frontend/vite.config.ts
        proxy: {
            '/api': { target: backend, changeOrigin: true, ws: true },
            '/.cratis': { target: backend, changeOrigin: true, ws: true }
        }
```

## Limits

Enums, nested lists and derived types are not generated. Header timestamps are omitted on purpose, which keeps output deterministic. See [Limits and gaps](/arc/backend/elixir/limits-and-gaps/).

## Next steps

- [Hosting on Phoenix](/arc/backend/elixir/hosting/phoenix/#a-react-app-on-the-same-origin) serves the built app from the same origin.
- [Samples](/arc/backend/elixir/samples/) includes the React app.
