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
| File | Effect constructs it uses | Written when |
|---|---|---|
http.ts | HttpApi, HttpApiGroup, HttpApiEndpoint, HttpApiBuilder, HttpRouter; exports AppRoutes | --emit all and there are locally implemented HTTP or RPC operations |
rpc.ts | Rpc, RpcGroup; exports Operations, OperationsHandlers, OperationsClient | --emit all and operations have @Rpc |
cli.ts | effect/cli Command; exports root and run(version) | --emit all and operations have @Cli |
client.ts | HttpApiClient, a Context.Service class, one function per operation, operationIndex | --emit all |
foldkit.ts | Foldkit Command adapters over client.ts | --emit all and at least one operation is annotated with Foldkit.Command |
guards.ts | declaration-only guard types (AppGuards) | --emit all and a local group needs guards |
<group>-contract.ts | a HttpApiGroup with its endpoint constants | --emit contract (every HTTP group) or all (declaration-only groups) |
<group>-handlers.ts | HttpApiBuilder.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 clientSTATE.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 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.
// 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 code | What to do instead |
|---|---|
| A DI container or service locator | Provide layers yourself, as in examples/users/src/server-main.ts: AppRoutes.pipe(Layer.provide(Users.layer), ...) |
| A copy of your schemas | Generated files import the original symbols (ADR 0004) |
| Changes to your handler bodies | Handlers stay as written. Generated files call them. |
| Runtime reflection of decorators | Decorators 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:
| Step | Why it works |
|---|---|
Remove .effx/ from .gitignore and commit .effx/generated | The directory is gitignored by default (.gitignore), so a fresh clone has no generated files until it runs effx build. |
Stop running effx build | The 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 layer | Each 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.