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.
Turn it on
Section titled “Turn it on”Add the :arc compiler and say where to write. Nothing is generated without both.
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:
mix arc.gen.proxiesmix arc.gen.proxies --application MyApp.Arc --output assets/src/arcmix arc.gen.proxies --check # write nothing; fail if the files are out of dateUse --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.
What you get
Section titled “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:
// 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
Section titled “Identity”The type named by use Arc.Identity.Provider, details: … is generated like any other, and the React app passes it to useIdentity:
// 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
Section titled “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
Section titled “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.tsis 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 diffstay 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
Section titled “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.
Point Vite at Phoenix
Section titled “Point Vite at Phoenix”In development, proxy the API and the Arc paths to Phoenix so the browser sees one origin:
proxy: { '/api': { target: backend, changeOrigin: true, ws: true }, '/.cratis': { target: backend, changeOrigin: true, ws: true } }Limits
Section titled “Limits”Enums, nested lists and derived types are not generated. Header timestamps are omitted on purpose, which keeps output deterministic. See Limits and gaps.
Next steps
Section titled “Next steps”- Hosting on Phoenix serves the built app from the same origin.
- Samples includes the React app.