Services and Actions
Define modular backend services and typed actions with runtime schema validation.
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:
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:
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
| Option | Type | Description |
|---|---|---|
database | DatabaseHandle | Database handle created via defineDatabase(). Accessible in actions as ctx.db. |
context | (baseCtx: BaseContext) => Promise<T> | T | Context 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
| Property | Type | Default | Description |
|---|---|---|---|
name | string | (Required) | Unique name within the service (e.g. "createOrder"). |
description | string | undefined | Human-readable description exported to the OpenAPI 3.1 operation. |
input | TSchema | undefined | TypeBox schema for validating input payload or query parameters. |
output | TSchema | undefined | TypeBox schema for validating return data. Mismatches throw ValidationError (status 500). |
method | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "POST" | HTTP method exposed on the API gateway. |
path | string | ${prefix}/${service}/${action} | Full HTTP route, including the prefix (it isn't prefixed for you). Can include /:id. |
authorize | (params) => boolean | Promise<boolean> | undefined | Guard run after input validation. Returning false rejects with UNAUTHORIZED (status 403). |
errors | Record<string, { status?, message }> | undefined | Custom error catalog added to error helpers. status defaults to 400. |
tags | string[] | [service name] | OpenAPI tags for the operation; also included in introspection. |
emits | string[] | undefined | Metadata 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:
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_1Both 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:
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.