Skip to content

Calling commands from code

Sometimes a command has to run without an HTTP client: an import job, a message consumer, an in-process spec, or a host that already speaks the Fetch API. ArcServer, available as app.server, has entry points for that. They differ in how much of the HTTP pipeline they run and in how much they trust the caller.

Entry pointRunsTrusts the caller with
handle(request, native?)The full HTTP pipelineNothing beyond an HTTP client
executeCommand(name, input, context)Authorization, binding, validators, and the commandThe whole execution context, including allowed severity
execute(command, context)The same, for a decorated command instanceThe same
validateCommand(name, input, context)Authorization, binding, and validators, without provide or handleThe whole execution context
validate(command, context)The same, for a decorated command instanceThe same
performQuery(name, input, context, options?)Authorization, binding, validators, and the queryThe whole execution context, except allowed severity

This example uses the Tasks sample’s classes, added explicitly:

import { randomUUID } from 'node:crypto';
import { ArcApplication, Severity } from '@cratis/arc.core';
import { Tasks } from './Features/Tasks/Tasks.js';
import { RegisterTask } from './Features/Tasks/Registration/Registration.js';
import { TaskItem } from './Features/Tasks/Listing/Listing.js';
const builder = ArcApplication.createBuilder();
builder.services.addSingleton(Tasks);
builder.add(RegisterTask, TaskItem);
const app = await builder.build();
const context = {
correlationId: randomUUID(),
principal: { id: 'import-job', roles: ['system'], isAuthenticated: true },
tenantId: 'acme',
signal: AbortSignal.timeout(5_000),
allowedSeverity: Severity.Warning
};
const result = await app.server.executeCommand('RegisterTask',
{ id: '1a638f8e-4444-4444-8888-a0b10cdd9977', title: 'Imported' }, context);
console.log(result.isSuccess, result.response);
const page = await app.server.performQuery('TaskItem.allTasks', {}, context, { paging: { page: 0, pageSize: 10 } });
console.log(page.data, page.paging);
await app.dispose();

The first log line is true 1a638f8e-4444-4444-8888-a0b10cdd9977; the query returns the imported task with paging.totalItems: 1.

executeCommand and performQuery look an operation up by its full name and throw when nothing matches:

ArtifactFull name
Model-bound commandNamespace and class name: Tasks.Registration.RegisterTask when discovered, RegisterTask when added without a namespace
Model-bound queryNamespace, read-model name, and method: Tasks.Listing.TaskItem.allTasks when discovered
Low-level definitionNamespace and name joined with a dot, such as Tasks.Create, or the bare name without a namespace

If you already hold a decorated command instance, app.server.execute(command, context) serializes its decorated fields and runs the same pipeline; the registered command name must be unambiguous. Call validateCommand(name, input, context) or validate(command, context) to check authorization and validation without running the command, like the /validate route. performQuery takes paging and sorting as its fourth argument. Import SortDirection from @cratis/arc.core, for example { paging: { page: 0, pageSize: 10 }, sorting: { field: 'title', direction: SortDirection.Ascending } }.

  • You supply the context. No authentication handler runs and no tenant is resolved. Authorization still runs against the principal and tenant you pass, so pass the real ones.
  • Allowed severity is yours to choose for commands. executeCommand uses context.allowedSeverity as given, including Severity.Error, which lets error-severity validation results pass. HTTP callers cannot do that. Only pass it from code entitled to override business rules. performQuery always uses Severity.Warning.
  • The context is ambient. Arc freezes a copy of your context and makes it available through currentContext() during the call. A nested call gets its own context, and the outer one is restored when it returns.
  • Nothing is redacted or logged. Exception messages and stack traces stay in the result, and the logger option is not called. Do not forward the result to an untrusted caller.
  • No body limit applies, because there is no body.

app.server.handle(request) takes a Fetch API Request and returns a Response, or null when the path is not an Arc route:

const response = await app.server.handle(new Request('http://localhost/api/register-task', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ id: '1a638f8e-4444-4444-8888-a0b10cdd9977', title: 'Via fetch' })
}));
console.log(response?.status, await response?.json());

It runs exactly what a host adapter runs: authentication handlers, tenant resolution, the body limit, the severity cap on X-Allowed-Severity, exception redaction, and the logger. The request’s signal becomes context.signal. The optional second argument supplies trusted native context.