Skip to content

React and generated proxies

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.

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

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:

Terminal window
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. Add remove_stale: false to the arc: options (or --no-remove-stale) to keep generated files for artifacts that no longer exist.

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:

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.

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

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.

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.

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.

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.

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

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

Enums, nested lists and derived types are not generated. Header timestamps are omitted on purpose, which keeps output deterministic. See Limits and gaps.