Getting started
Walk the examples/users slice end to end, from declarations to check, build, inspect, graph, and a running server, client and CLI.
This page follows the examples/users slice. Every code block below is included from the real file, so it cannot drift from the repository.
effx is not published. @effx/cli and @effx/runtime are workspace packages at version 0.1.0 (packages/cli/package.json) and no npm install flow exists yet. Every command here runs from a checkout of the monorepo. STATE.md records that mono-web consumes a locally packed, vendored distribution. The root script bun run pack builds the local tarballs (package.json, scripts/pack.ts).
Two ways to call the CLI today
| Where | Command | Notes |
|---|---|---|
| Monorepo root | bun run effx <command> --project examples/users/tsconfig.json | Root script effx is bun scripts/effx.ts. --project is resolved against the working directory. |
| Example package | bun run --cwd examples/users effx:build | The script effx:build in examples/users/package.json is bun ../../scripts/effx.ts build. --project defaults to tsconfig.json in that directory. |
| Published package | not available | Nothing is published. Do not rely on a bunx effx or npm install flow. |
Install the workspace dependencies first with bun install at the repository root. AGENTS.md notes that the Oxlint plugin dependency is a local path pin to a sibling checkout.
What you build
examples/users/src/
schemas.ts Effect Schemas: ids, inputs, views, tagged errors
user.ts @PersistentModel User with Public and Self views
services.ts Users service + in-memory layer
capabilities.ts Read, ChangeEmail capabilities
operations.ts the two annotated operations
│ effx build
▼
examples/users/.effx/
ir.json manifest.json generated/{http,rpc,cli,client}.tsSource: docs/specs/0003-cli-and-vertical-slice.md "Journey" and "examples/users".
1. Declare
Schemas are plain Effect Schema values. The compiler keeps them as references and never serializes them (Schema refs).
import { Schema } from "effect";
export const UserId = Schema.String.pipe(Schema.brand("UserId"));
export const Email = Schema.NonEmptyString;
export const GetUserInput = Schema.Struct({ id: UserId });
export const ChangeEmailInput = Schema.Struct({ id: UserId, email: Email });
export const UserPublic = Schema.Struct({ id: UserId, displayName: Schema.String });
export const UserSelf = Schema.Struct({ id: UserId, email: Email, displayName: Schema.String });
export class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", {
id: UserId,
}) {}
export class EmailTaken extends Schema.TaggedError<EmailTaken>()("EmailTaken", {
email: Email,
}) {}
The model ties a table and two views to the User class.
import { PersistentModel } from "@effx/runtime";
import { Schema } from "effect";
import { Email, UserId, UserPublic, UserSelf } from "./schemas.ts";
@PersistentModel({
table: "users",
views: { Public: UserPublic, Self: UserSelf },
focus: { email: ["email"] },
})
export class User extends Schema.Class<User>("User")({
id: UserId,
email: Email,
displayName: Schema.String,
}) {
static readonly Public = UserPublic;
static readonly Self = UserSelf;
}
The service is an ordinary Context.Service. The layer is an in-memory store, not durable across restarts.
import { Context, Effect, HashMap, Layer, Option, Ref, Result } from "effect";
import { Email, EmailTaken, UserId, UserNotFound } from "./schemas.ts";
import { User } from "./user.ts";
/** An in-process example store. Successful writes are visible to this layer's readers, but are not durable across restarts. */
export class Users extends Context.Service<
Users,
{
readonly find: (id: typeof UserId.Type) => Effect.Effect<User, UserNotFound>;
readonly setEmail: (
id: typeof UserId.Type,
email: typeof Email.Type,
) => Effect.Effect<User, UserNotFound | EmailTaken>;
}
>()("@effx-examples/users/src/services/Users") {
static readonly layer = Layer.effect(
Users,
Effect.gen(function* () {
const alice = new User({
id: UserId.make("1"),
email: "alice@example.com",
displayName: "Alice",
});
const bob = new User({ id: UserId.make("2"), email: "bob@example.com", displayName: "Bob" });
const users = yield* Ref.make<HashMap.HashMap<typeof UserId.Type, User>>(
HashMap.make([alice.id, alice], [bob.id, bob]),
);
const find = Effect.fn("Users.find")(function* (id: typeof UserId.Type) {
const user = HashMap.get(yield* Ref.get(users), id);
if (Option.isNone(user)) return yield* new UserNotFound({ id });
return user.value;
});
const setEmail = Effect.fn("Users.setEmail")(function* (
id: typeof UserId.Type,
email: typeof Email.Type,
) {
const result = yield* Ref.modify(
users,
(
current,
): readonly [
Result.Result<User, UserNotFound | EmailTaken>,
HashMap.HashMap<typeof UserId.Type, User>,
] => {
const found = HashMap.get(current, id);
if (Option.isNone(found)) {
return [Result.fail(new UserNotFound({ id })), current];
}
for (const [otherId, other] of current) {
if (otherId !== id && other.email === email) {
return [Result.fail(new EmailTaken({ email })), current];
}
}
const updated = new User({
id: found.value.id,
email,
displayName: found.value.displayName,
});
return [Result.succeed(updated), HashMap.set(current, id, updated)];
},
);
return yield* Effect.fromResult(result);
});
return Users.of({ find, setEmail });
}),
);
}
Capabilities name what an operation is allowed to do, against a model and an optional focus path.
import { Capability, Focus } from "@effx/runtime";
import { User } from "./user.ts";
export const Read = Capability.make("User.Read", { resource: User });
export const ChangeEmail = Capability.make("User.ChangeEmail", {
resource: User,
focus: Focus.key(User, "email"),
});
The operations carry the annotations. Each decorator contributes to the IR. None of them changes how the method runs (The invariant).
import { Authorize, Cli, Command, Errors, Http, Query, Requirements, Rpc } from "@effx/runtime";
import { Effect } from "effect";
import { ChangeEmail, Read } from "./capabilities.ts";
import { ChangeEmailInput, EmailTaken, GetUserInput, UserNotFound } from "./schemas.ts";
import { Users } from "./services.ts";
import { User } from "./user.ts";
export class UserOperations {
@Query({ name: "User.Get", input: GetUserInput, success: User.Public })
@Http.Get("/users/:id")
@Rpc("User.Get")
@Cli("users get")
@Authorize(Read)
static get(input: typeof GetUserInput.Type) {
return Effect.gen(function* () {
const users = yield* Users;
const user = yield* users.find(input.id);
return { id: user.id, displayName: user.displayName };
});
}
@Command({ name: "User.ChangeEmail", input: ChangeEmailInput, success: User.Self })
@Http.Patch("/users/:id/email")
@Rpc("User.ChangeEmail")
@Cli("users change-email")
@Authorize(ChangeEmail)
@Errors(UserNotFound, EmailTaken)
@Requirements(Users)
static changeEmail(input: typeof ChangeEmailInput.Type) {
return Effect.gen(function* () {
const users = yield* Users;
return yield* users.setEmail(input.id, input.email);
});
}
}
| Annotation | What it contributes |
|---|---|
@Query, @Command | An Operation node with input and success schema references |
@Http.Get, @Http.Patch | An Exposure node with an ExposedAs edge |
@Rpc, @Cli | The same, for RPC and CLI exposure |
@Authorize | An AuthorizedBy edge to a capability |
@Errors, @Requirements | Declared error and requirement sets, checked against what the handler infers |
Source: docs/specs/0001-compiler-kernel.md annotation argument shapes. The authoring surface is documented under /docs/authoring/http.
2. Check
check runs collect, interpret and analyze. It prints diagnostics and writes nothing.
bun run effx check --project examples/users/tsconfig.jsonThe output ends with a summary line, N error(s), M warning(s), K info. The exit code is 1 if any diagnostic has severity error, otherwise 0. See effx check.
3. Build
bun run --cwd examples/users effx:buildbuild runs check, then writes .effx/ir.json, .effx/manifest.json and the generated files. STATE.md records this build as zero errors, four generated files, and TypeScript-version skew reported as EFFX0001 (later entries add two unprotected-User EFFX2504 warnings). See effx build.
.effx/ is gitignored. The example tsconfig.json includes .effx/generated/**/*.ts, so run the build before typechecking or running the example (spec 0003).
4. Inspect
bun run effx inspect User.ChangeEmail --project examples/users/tsconfig.jsoninspect prints one operation's contract and exposures. The format is defined in spec 0003. This block is the spec's format example, not captured from a run for this page.
Operation User.ChangeEmail
Kind
Command
Contract
ChangeEmailInput → User.Self
Errors
EmailTaken
UserNotFound
Requirements
Users
Authority
User.ChangeEmail
resource: User
focus: User.email
Exposed
PATCH /users/:id/email
RPC User.ChangeEmail
CLI users change-emailSee effx inspect.
5. Graph
bun run effx graph User.Get --project examples/users/tsconfig.jsongraph prints a Mermaid flowchart LR of the whole IR, or of one node plus everything reachable from it. See effx graph.
6. Run it
The generated http.ts exports AppRoutes. server.ts re-exports it and server-main.ts is the composition root. You provide the layers. effx adds no runtime of its own.
import { BunHttpServer, BunRuntime } from "@effect/platform-bun";
import { Config, Effect, Layer } from "effect";
import { AppRoutes } from "./server.ts";
import { Users } from "./services.ts";
const Server = Layer.unwrap(
Effect.map(Config.Number("PORT").pipe(Config.withDefault(3000)), (port) =>
AppRoutes.pipe(Layer.provide(Users.layer), Layer.provide(BunHttpServer.layer({ port }))),
),
);
BunRuntime.runMain(Layer.launch(Server));
The examples/users/package.json scripts:
| Script | Command it runs | Needs |
|---|---|---|
effx:build | bun ../../scripts/effx.ts build | nothing |
serve | bun src/server-main.ts | a prior build. Reads PORT (default 3000). |
client | bun src/client-main.ts | a prior build and a running server. Reads EFFX_URL (default http://localhost:3000). |
cli | bun src/cli-main.ts | a prior build |
Run each with bun run --cwd examples/users <script>. STATE.md records these runs from the CLI/users evidence:
PORT=31063 bun run --cwd examples/users serve
EFFX_URL=http://127.0.0.1:31063 bun run --cwd examples/users client
bun run --cwd examples/users cli users get --input '{"id":"1"}'
bun run --cwd examples/users cli users change-email --input '{"id":"1","email":"alice+cli@example.com"}'The recorded results: GET /users/1 and PATCH /users/1/email returned HTTP 200, the client logged the public and self views, and each generated CLI command printed one JSON success line.