brick.ts

Services and Actions

Define modular backend services and typed actions with runtime schema validation.

llms.txt

Services structure backend domains in Brick-TS, while actions provide typed business logic executable both in-process and over HTTP.

Minimal Example

Define a service, register an action with TypeBox input/output schemas, and invoke it either directly in TypeScript or through HTTP:

src/services/users.ts
import { defineService, t } from "@brickkit/core";

// Mock data store
const users = new Map([
  ["usr_1", { id: "usr_1", name: "Ada Lovelace", email: "ada@example.com" }],
]);

// 1. Define the service domain
export const usersService = defineService("users");

// 2. Define a typed action
export const getUser = usersService.action({
  name: "getUser",
  description: "Retrieve a user profile by unique ID",
  method: "GET",
  path: "/api/users/:id",
  input: t.Object({
    id: t.String({ minLength: 1 }),
  }),
  output: t.Object({
    id: t.String(),
    name: t.String(),
    email: t.String(),
  }),
  execute: ({ input, error }) => {
    const user = users.get(input.id);
    if (!user) {
      // Error helpers throw; `return` lets TypeScript narrow `user`
      return error.NOT_FOUND(`User with ID '${input.id}' not found`);
    }
    return user;
  },
});

Brick doesn't register TypeBox string formats such as email or uuid. A schema with an unregistered format fails validation ("Unknown format"), so register formats with TypeBox's FormatRegistry before using them.

Services (defineService)

A service groups a database handle, a context resolver, resources, and custom actions:

src/services/orders.ts
import { defineDatabase, defineService } from "@brickkit/core";
import { ordersTable } from "./schema";

const database = defineDatabase({ tables: { ordersTable } });

export const ordersService = defineService("orders", {
  database,
  context: async (baseCtx) => {
    const token = baseCtx.request.headers.get("authorization");
    return { token, tenantId: "tenant_default" };
  },
});

Service Options Table

OptionTypeDescription
databaseDatabaseHandleDatabase handle created via defineDatabase(). Accessible in actions as ctx.db.
context(baseCtx: BaseContext) => Promise<T> | TContext resolver function. Injects user, session, or tenant properties into every action.

ServiceOptions also declares auth, rateLimit, cache, resilience, telemetry, and lifecycle. The runtime doesn't act on them today: no lifecycle hooks run and no rate limits or auth defaults are enforced. They're only echoed in the service's introspection output (/_brick/services). Enforce access with authorize on actions and access on CRUD resources.

Actions (service.action / defineAction)

Actions are the core unit of compute in Brick-TS. Every action is registered with TypeBox schemas and an execution function.

Action Configuration Table

PropertyTypeDefaultDescription
namestring(Required)Unique name within the service (e.g. "createOrder").
descriptionstringundefinedHuman-readable description exported to the OpenAPI 3.1 operation.
inputTSchemaundefinedTypeBox schema for validating input payload or query parameters.
outputTSchemaundefinedTypeBox schema for validating return data. Mismatches throw ValidationError (status 500).
method"GET" | "POST" | "PUT" | "PATCH" | "DELETE""POST"HTTP method exposed on the API gateway.
pathstring${prefix}/${service}/${action}Full HTTP route, including the prefix (it isn't prefixed for you). Can include /:id.
authorize(params) => boolean | Promise<boolean>undefinedGuard run after input validation. Returning false rejects with UNAUTHORIZED (status 403).
errorsRecord<string, { status?, message }>undefinedCustom error catalog added to error helpers. status defaults to 400.
tagsstring[][service name]OpenAPI tags for the operation; also included in introspection.
emitsstring[]undefinedMetadata included in introspection.
execute({ input, ctx, error }) => Promise<T> | T(Required)Implementation function. Receives validated input, resolved context, and error helpers.

Monomorphic Execution (In-Process vs HTTP)

An action can be called in-process without network overhead or data serialization:

src/example-call.ts
import { getUser } from "./services/users";

// Direct in-process execution with full validation and authorization:
const user = await getUser({ input: { id: "usr_1" } });
console.log(user.name); // "Ada Lovelace"

// Alternative shorthand:
const result = await getUser.run({ id: "usr_1" });

When the app is served via brick(), the gateway mounts the action as an HTTP endpoint:

curl http://localhost:4000/api/users/usr_1

Both invocations run the same steps in the same order: input validation, authorize, execute, then output validation.

Path Parameters and Custom Routes

When an action declares path with parameters (e.g., /api/users/:id), Brick merges path parameters into input. Declare them in the input schema so they're validated:

src/services/users.ts
export const deleteUser = usersService.action({
  name: "deleteUser",
  method: "DELETE",
  path: "/api/users/:id",
  input: t.Object({
    id: t.String(),
  }),
  output: t.Object({
    success: t.Boolean(),
  }),
  execute: async ({ input, error }) => {
    return { success: true };
  },
});

When called via HTTP DELETE /api/users/usr_1, Brick extracts id from the path and validates it against input. When called in-process, pass { id: "usr_1" } in the input object.