Build an Arc application
An application builder collects your commands, read models, validators, and services, checks that they fit together, and creates the ArcServer that every host uses. You describe the application once; the host is a separate choice.
Create, fill, and build
Section titled “Create, fill, and build”The Tasks entry point shows discovery, service registration, and generated metadata in one place. For an artifact with no injected parameters, you can also add it explicitly:
import { field } from '@cratis/fundamentals';import { query, readModel } from '@cratis/arc.core';
@readModel()export class Status { @field(String) value = 'ready';
@query() static current(): Status { return new Status(); }}import { ArcApplication } from '@cratis/arc.core';import { Status } from './Status.js';
const builder = ArcApplication.createBuilder();builder.add(Status);export const app = await builder.build();await app.run({ port: Number(process.env.PORT ?? 3000) });The Tasks command and queries do take parameters. In the Tasks bootstrap, useGeneratedMetadata(metadata) installs the bindings that arc-proxygenerator --metadata extracted from the source, so handle(tasks: Tasks) and a bare @query() need no token lists. Call it before discover() or add(). Without it, the sample’s artifacts would need explicit @inject(...) and @query(...) tokens, and build() rejects them as written. See Generate artifact metadata.
The Tasks sample sets Development in Samples/Tasks/appsettings.json; run the workspace command from its package directory (Yarn does this). createBuilder accepts the configuration options (ArcOptions). build() returns an ArcApplication, and app.server is its ArcServer. This TypeScript setup corresponds to C#‘s standalone ArcApplication.CreateBuilder(args), builder.AddCratisArc(), builder.Build(), app.UseCratisArc(), app.RunAsync(). On Node, app.run() performs the standalone host step. For an Arc + Chronicle comparison, see Add event sourcing.
Before a listener opens, build() checks the declared graph: missing service registrations, dependency cycles, singletons that capture shorter-lived services, and decorators placed where they have no effect. It never runs a service factory to do this.
Discover artifacts from a folder
Section titled “Discover artifacts from a folder”discover(folderUrl, { rootNamespace? }) imports every exported decorated class below the folder, in deterministic path order.
- It accepts emitted
.jsfiles or loader-backed.tsfiles, not a mix of both. - It skips
dist,node_modules,given,for_*,index.*, declaration files, symbolic links, co-located*.proxy.ts/*.proxy.jsfiles, and React.tsxfiles. - It refuses a folder that contains the entry point, or an imported bootstrap that is itself calling discovery. Keep artifacts in a dedicated folder.
- It derives each artifact’s namespace from its path below the folder, and reports a class discovered under two different namespaces.
A folder rename changes the derived routes. Endpoint mapping shows how to pin a route.
Add artifacts explicitly
Section titled “Add artifacts explicitly”Bundled production builds often cannot import a folder at runtime. Pass the classes instead:
builder.add(RegisterTask, RegisterTaskValidator, TaskItem);add() rejects a class without an Arc decorator. Give explicitly added artifacts stable namespaces with @command({ namespace: 'Tasks.Registration' }) and @readModel({ namespace: 'Tasks.Listing' }). If you rely on class names in routes, configure the bundler to keep them (keepNames in esbuild), or set explicit namespaces and paths.
Besides commands, read models, and validators, add() and discover() recognize classes marked @queryRenderer(), @readModelInterceptor(), @commandResponseValueHandler(), @identityDetailsProvider(), and the lifetime decorators @singleton(), @scoped(), and @transient().
Register extension points
Section titled “Register extension points”The builder also registers services that change pipeline behavior:
| Builder method | Registers |
|---|---|
services.addSingleton / addScoped / addTransient | An application service; see Dependency injection |
addAuthorizationPolicy(name, policy) | A named policy; see Authorization policies |
addCommandResponseValueHandler(token) | A handler for server-side return values; see Response value handlers |
addCommandKeyResolver(token) | A rule that computes a command key; see Command context |
addCommandContextValuesProvider(token) | Values attached to every command context |
addReadModelForCommandResolver(token) | A source for commandReadModel(...) parameters |
addQueryRenderer(token) | A renderer for provider-owned query results; see Query renderers |
addReadModelInterceptor(token) | A read-model transform; see Read-model interception |
After importing their packages, call builder.withMongoDB(...), builder.withDrizzle(...), or builder.withChronicle(...). Importing the package adds its method to the builder. Each package also exports a function form, withX(builder, options). Configuration from appsettings.json is available to Chronicle and MongoDB, but model classes, clients, and authentication must be provided explicitly.
Run it, or mount it
Section titled “Run it, or mount it”await app.run(...)orawait app.start(...)use the standalone Node host.expressApp.use(cratisArc(app)),await fastifyApp.register(cratisArc, { arc: app }), andhonoApp.use(cratisArc(app))hand the application to a web framework. ImportcratisArcfrom the matching adapter package. The host keeps listener ownership; callawait app.dispose()at shutdown. See Host adapters.app.fetch(request)returns an Arc response or 404 for foreign paths;app.handle(request)returnsnullfor fall-through. These Fetch methods alone do not establish edge-runtime compatibility: the core still imports Node runtime modules.
A caller-owned ServiceRegistry passed in the options cannot be combined with builder service registrations.