Consume generated proxies
A backend change should produce a compiler error where your frontend needs updating, not a surprise in production. Arc generates TypeScript command, query, and model types from discovered endpoints — for the shared frontend runtime. Run the generator for your backend, then update your consumers; never edit generated files.
You get completion for command fields and query arguments, typed responses, and compiler-guided repairs after a backend rename. The generated modules also give you concrete contracts to navigate instead of hand-maintained request shapes.
Set up the frontend
Section titled “Set up the frontend”Current generated command and query modules import both @cratis/arc and @cratis/arc.react, even when you call them imperatively. Install their dependencies in your React app:
npm install @cratis/fundamentals @cratis/arc @cratis/arc.react react react-domMount Arc above hook consumers. For build configuration, namespace mapping, source-file grouping, and output ownership, use the C# backend proxy-generation reference, the JVM TypeScript proxies guide, or the TypeScript source preview’s generator setup.
Commands use your application services
Section titled “Commands use your application services”Arc is a standalone CQRS framework. A command can call an ordinary service and return a response; it does not need events or an event log.
This backend fragment assumes your application implements and registers the account service. That interface is application-owned, not an Arc API:
using Cratis.Arc.Commands.ModelBound;using Cratis.Concepts;
public record AccountName(string Value) : ConceptAs<string>(Value){ public static readonly AccountName NotSet = new(string.Empty);}
public record AccountBalance(decimal Value) : ConceptAs<decimal>(Value){ public static readonly AccountBalance NotSet = new(0m);}
public interface IAccountService{ Task<Guid> Open(AccountName name, AccountBalance initialBalance);}
[Command]public record OpenDebitAccount(AccountName Name, AccountBalance InitialBalance){ public Task<Guid> Handle(IAccountService accounts) => accounts.Open(Name, InitialBalance);}import io.cratis.arc.concepts.ConceptAs;import java.math.BigDecimal;import java.util.UUID;
public record AccountName(String value) implements ConceptAs<String> {}
public record AccountBalance(BigDecimal value) implements ConceptAs<BigDecimal> {}
public interface AccountService { UUID open(AccountName name, AccountBalance initialBalance);}
@Commandpublic record OpenDebitAccount(AccountName name, AccountBalance initialBalance) { public UUID handle(@FromServices AccountService accounts) { return accounts.open(name, initialBalance); }}import io.cratis.arc.concepts.ConceptAsimport java.math.BigDecimalimport java.util.UUID
data class AccountName(private val rawValue: String) : ConceptAs<String> { override fun value(): String = rawValue}
data class AccountBalance(private val rawValue: BigDecimal) : ConceptAs<BigDecimal> { override fun value(): BigDecimal = rawValue}
interface AccountService { suspend fun open(name: AccountName, initialBalance: AccountBalance): UUID}
@Commanddata class OpenDebitAccount(val name: AccountName, val initialBalance: AccountBalance) { suspend fun handle(@FromServices accounts: AccountService): UUID = accounts.open(name, initialBalance)}import { ConceptAs, field, Guid } from '@cratis/fundamentals';import { command, inject } from '@cratis/arc.core';
export class AccountName extends ConceptAs<string> { static readonly valueType = String;}
export class AccountBalance extends ConceptAs<number> { static readonly valueType = Number;}
export abstract class AccountService { abstract open(name: AccountName, initialBalance: AccountBalance): Promise<Guid>;}
@command()export class OpenDebitAccount { @field(AccountName) name!: AccountName; @field(AccountBalance) initialBalance!: AccountBalance;
@inject(AccountService) handle(accounts: AccountService): Promise<Guid> { return accounts.open(this.name, this.initialBalance); }}The named concepts keep account names and balances distinct in backend signatures. Their generated client properties use the underlying primitive — here string and number — so the form does not need to construct concept objects. Keep each concept in its own file when organizing your application.
After generation, use the proxy from a React component. This consumer fragment assumes its generated import path is ./generated/OpenDebitAccount:
import { OpenDebitAccount } from './generated/OpenDebitAccount';
export function OpenAccountButton() { const [command] = OpenDebitAccount.use({ name: 'Primary account', initialBalance: 500 });
async function open() { const result = await command.execute(); if (result.isSuccess) { console.log(String(result.response)); } }
return <button onClick={() => void open()}>Open account</button>;}An ordinary Guid/UUID response remains a supported response and maps to Fundamentals Guid. The Chronicle integration adds optional event-sourcing behavior; it does not redefine generic Arc responses.
The command hook returns [command, setValues, clearValues]. Set required content explicitly. The setter edits properties; it does not reset the change-tracking baseline. See data binding.
Queries follow the backend result shape
Section titled “Queries follow the backend result shape”Both controller-based and model-bound C# endpoints generate proxies; the JVM generates from model-bound @ReadModel query methods only — it has no controller-based discovery. TypeScript’s source generator reads @readModel() classes with static @query() methods; see its proxy generation overview for supported output. Parameterized queries emit a NameParameters interface. Query hooks return tuples, not model objects; read the first tuple element’s .data.
- Ordinary
.use()returns[result, perform, setSorting]. - Observable enumerable
.use()returns[result, setSorting]. - Observable single-model
.use()returns[result]. - Only enumerable proxies expose
.useWithPaging(),.useSuspenseWithPaging(), and, for observables,.useChangeStream().
See query usage for argument positions and the complete tuple matrix. Results expose isPerforming, isReady, isAuthorized, isValid, hasExceptions, and exceptionMessages; they do not expose an error or isLoading property.
Live and Suspense consumers
Section titled “Live and Suspense consumers”Observable proxies subscribe to an emitting backend source. MongoDB observation is one option; Chronicle is another optional integration. A database write does not become a live notification without that source wiring. Both SSE and WebSocket support direct and shared-hub modes.
All generated queries expose .useSuspense(). Suspense handles initial waiting and selected failures, not every unsuccessful result: keep validation/readiness handling in the component. Read Suspense limitations before adding an error boundary or retry control.