effx
Introduction

Generated code

effx writes ordinary, readable and ejectable Effect files (HttpApi, Rpc, CLI, client, Foldkit) with no effx runtime or dependency injection.

Generated output is ordinary Effect. You can read it, diff it, keep it, and replace any layer of it with hand-written Effect. There is no effx container, runtime registry or reflection (ADR 0008). Each generating annotation maps to a named Effect construct; the table, including the annotations that generate nothing, is in the Introduction.

Source: docs/decisions/0008-generated-code-is-ordinary-effect.md, docs/decisions/0012-positioning-enterprise-on-ramp.md, packages/compiler/src/generate/.

What each file is

FileEffect constructs it usesWritten when
http.tsHttpApi, HttpApiGroup, HttpApiEndpoint, HttpApiBuilder, HttpRouter; exports AppRoutes--emit all and there are locally implemented HTTP or RPC operations
rpc.tsRpc, RpcGroup; exports Operations, OperationsHandlers, OperationsClient--emit all and operations have @Rpc
cli.tseffect/cli Command; exports root and run(version)--emit all and operations have @Cli
client.tsHttpApiClient, a Context.Service class, one function per operation, operationIndex--emit all
foldkit.tsFoldkit Command adapters over client.ts--emit all and at least one operation is annotated with Foldkit.Command
guards.tsdeclaration-only guard types (AppGuards)--emit all and a local group needs guards
<group>-contract.tsa HttpApiGroup with its endpoint constants--emit contract (every HTTP group) or all (declaration-only groups)
<group>-handlers.tsHttpApiBuilder.group(...).handleRaw(...) factory--emit handlers or all, for declaration-only groups

Source: packages/compiler/src/generate/{http,rpc,cli,client,foldkit,guards}.ts. Each generator returns an empty list when its condition is not met. No mode writes an empty placeholder file for a missing implementation (spec 0010).

Emit modes are explained in Emit modes and targets. Declaration-only groups are explained in External bindings.

File layout

effx build writes under .effx/, next to the tsconfig. For examples/users the layout is:

examples/users/.effx/
├── ir.json            canonical IR (one line, trailing newline)
├── manifest.json      semantic hash, locations, diagnostics, generated file list
└── generated/
    ├── http.ts        HttpApi + AppRoutes
    ├── rpc.ts         Rpc group + handlers layer
    ├── cli.ts         effect/cli command tree
    └── client.ts      typed client

STATE.md records "four generated files" for the examples/users build. .effx/ is gitignored (.gitignore), so these files do not exist until you run the build. See Getting started.

A declaration-only project (spec 0010) writes different names. With --emit contract and a group named profile it writes profile-contract.ts only. With --emit handlers it writes profile-handlers.ts only.

Every file starts the same way

Each file begins with a header. It names the source symbols and the IR ids it came from.

generated header
// Generated by effx. Do not edit.
// Source: <handler symbols>
// IR: <operation ids>

Source: HEADER and header in packages/compiler/src/generate/emit.ts. For a declaration-only operation the Source line reads (none), because no local handler exists.

Output is deterministic. generate sorts all files by path (packages/compiler/src/pipeline.ts).

A real generated file

The file below is a checked-in generated snapshot used by packages/frontend-ts/test/client-foldkit.test.ts. STATE.md says Vitest compares each snapshot file with the current compiler projection. It is a Profile-like fixture, not the users example. It shows plain HttpApiGroup, HttpApiEndpoint and HttpApiBuilder calls, and imports of the original schema symbols.

packages/frontend-ts/test/fixtures/users/.effx/client-foldkit-generated/http.ts
// Generated by effx. Do not edit.
// Source: ../../src/operations.client-foldkit#ClientFoldkitInternalOperations.read, ../../src/operations.client-foldkit#ClientFoldkitProfileOperations.read, ../../src/operations.client-foldkit#ClientFoldkitProfileOperations.update
// IR: operation:Internal.ProfileRead, operation:Profile.Read, operation:Profile.Update

import { Effect, Layer, Schema } from "effect";
import { HttpRouter } from "effect/http";
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api";
import { ProfileCredentialSecurity, ProfileCurrentPerson, ProfileProblemResponses, ProfileReadHeaders, ProfileReadQuery, ProfileResponse, ProfileResponseHeaders, ProfileUpdateInput, ProfileWriteHeaders, profileAccessAnnotations } from "../../src/client-foldkit-support.ts";
import { ClientFoldkitInternalOperations, ClientFoldkitProfileOperations } from "../../src/operations.client-foldkit.ts";

import type { AppGuards, ProfileGuards } from "./guards.ts";

export class ProfileGroup extends HttpApiGroup.make("profile").add(
  HttpApiEndpoint.get("read", "/api/profile", {
  query: ProfileReadQuery,
  headers: ProfileReadHeaders,
  success: HttpApiSchema.WithHeaders(ProfileResponse, ProfileResponseHeaders),
  error: ProfileProblemResponses("readProblem", ["credential.missing"]),
}).middleware(ProfileCredentialSecurity).annotateMerge(OpenApi.annotations({ identifier: "profile.read" })).annotateMerge(profileAccessAnnotations({ exposure: "External", acceptedCredentials: ["BetterAuthCookie","OAuthUserBearer"], principalKinds: ["Person"], capabilities: {"_tag":"One","capability":"profile.read-self"}, requirements: [{"id":"profile.owner"}], canonicalScopeResolver: ProfileCurrentPerson, concealment: {"_tag":"Reveal"}, decisionTime: "SnapshotRead" })),
  HttpApiEndpoint.patch("update", "/api/profile", {
  headers: ProfileWriteHeaders,
  payload: ProfileUpdateInput,
  success: HttpApiSchema.WithHeaders(ProfileResponse, ProfileResponseHeaders),
  error: ProfileProblemResponses("updateProblem", ["credential.missing"]),
}).middleware(ProfileCredentialSecurity).annotateMerge(OpenApi.annotations({ identifier: "profile.update" })).annotateMerge(profileAccessAnnotations({ exposure: "External", acceptedCredentials: ["BetterAuthCookie","OAuthUserBearer"], principalKinds: ["Person"], capabilities: {"_tag":"One","capability":"profile.update-self"}, requirements: [{"id":"profile.owner"}], canonicalScopeResolver: ProfileCurrentPerson, concealment: {"_tag":"Reveal"}, decisionTime: "Transaction" })),
) {}

export class InternalSecretsGroup extends HttpApiGroup.make("secrets").add(
  HttpApiEndpoint.get("Internal.ProfileRead", "/internal/profile", {
  success: ProfileResponse,
  error: Schema.Never,
}).annotateMerge(profileAccessAnnotations({ exposure: "Internal", acceptedCredentials: ["None"], principalKinds: ["Anonymous"], capabilities: {"_tag":"None"}, requirements: [], canonicalScopeResolver: ProfileCurrentPerson, concealment: {"_tag":"Reveal"}, decisionTime: "SnapshotRead" })),
) {}

export class Api extends HttpApi.make("effx").add(ProfileGroup) {}
export class InternalApi extends HttpApi.make("internal").add(InternalSecretsGroup) {}

export const ProfileHandlers = (guards: ProfileGuards) => HttpApiBuilder.group(
  Api,
  "profile",
  Effect.fn(function* (handlers) {
    return handlers.handleAll({
      "read": ({ request, query, headers }) => ClientFoldkitProfileOperations.read({ ...query, ...headers }, () => guards["read"](request)),
      "update": ({ request, headers, payload }) => ClientFoldkitProfileOperations.update({ ...headers, ...payload }, () => guards["update"](request)),
    });
  }),
);

export const InternalSecretsHandlers = HttpApiBuilder.group(
  InternalApi,
  "secrets",
  Effect.fn(function* (handlers) {
    return handlers.handleAll({
      "Internal.ProfileRead": ({}) => ClientFoldkitInternalOperations.read({}),
    });
  }),
);

export const AppRoutes = (guards: AppGuards) => HttpRouter.serve(Layer.mergeAll(HttpApiBuilder.layer(Api).pipe(Layer.provide([ProfileHandlers(guards["effx/profile"])])), HttpApiBuilder.layer(InternalApi).pipe(Layer.provide([InternalSecretsHandlers]))));

The generated group is a value you could have written yourself. Handlers receive decoded input and call your operation function. The operation function is yours, not generated.

What generated code does not do

Not in generated codeWhat to do instead
A DI container or service locatorProvide layers yourself, as in examples/users/src/server-main.ts: AppRoutes.pipe(Layer.provide(Users.layer), ...)
A copy of your schemasGenerated files import the original symbols (ADR 0004)
Changes to your handler bodiesHandlers stay as written. Generated files call them.
Runtime reflection of decoratorsDecorators are not read at runtime by generated code

Ejecting

Generated code stays readable and ejectable (ADR 0012, "Consequences"). There is no eject command in the CLI (scripts/effx.ts has check, build, inspect, graph). Ejecting is a version-control decision:

StepWhy it works
Remove .effx/ from .gitignore and commit .effx/generatedThe directory is gitignored by default (.gitignore), so a fresh clone has no generated files until it runs effx build.
Stop running effx buildThe generated files import effect/* modules, your own schema and handler modules, and sibling generated files. They import nothing from @effx/* (see examples/users/.effx/generated/http.ts).
Edit the files by hand, or replace one layerEach file is a plain HttpApiGroup, RpcGroup, Command or client you could have written. The header says "Do not edit" because effx build rewrites the file.

Two limits to know. Your handler module still imports the decorators from @effx/runtime, which are no-ops or metadata only (ADR 0001); the generated files call the handler method and do not read the annotations. And effx build writes every generated file with writeFileString, so a later build overwrites hand edits (packages/cli/src/commands.ts).

Targets

Generated imports use the sole stable effect-4.0 target and native effect/http-api paths. Prerelease Effect targets are not supported. Source: packages/compiler/src/generate/target.ts. See Emit modes and targets.

On this page