Adding HTTP Endpoints to an Effect Golem Agent
Effect agents publish HTTP route metadata with the Http namespace from
@golemcloud/effect-golem. The Golem host serves the routes, decodes path, query, header, and JSON
body values into method parameters, invokes the agent, and maps the method result to an HTTP
response.
Steps
- Define the agent and its methods with
defineAgent,method, and Effect Schema. - Add
http: Http.mount(...)to the agent definition. - Add an
http: [Http.get(...) | Http.post(...) | ...]array to every exposed method. - Implement every handler as an
Effectand import its module fromsrc/main.ts. - Add the agent type to an
httpApidomain deployment ingolem.yaml. - Run
golem build, then deploy withgolem deploy --yes.
Related Skills
| Skill | When to Load |
|---|---|
golem-add-agent-effect |
Defining the Effect agent, method schemas, and durable state |
golem-http-params-effect |
Detailed path, query, header, body, and response mapping |
golem-make-http-request-effect |
Making outgoing HTTP requests from an Effect handler |
golem-add-http-auth-effect |
Enabling authentication and reading the caller principal |
golem-add-cors-effect |
Configuring mount-level or endpoint-level CORS |
golem-configure-api-domain |
Configuring the httpApi deployment domain |
Mount Path
Import Http from the Effect Golem SDK and put one mount on the agent definition:
import { Effect, Schema } from "effect";
import { defineAgent, Http, method } from "@golemcloud/effect-golem";
export const TaskAgent = defineAgent({
name: "TaskAgent",
mode: "durable",
id: {
taskName: Schema.String,
},
http: Http.mount("/api/tasks/{taskName}"),
methods: {
// ...
},
}).implement({
init: () => Effect.void,
methods: () => ({
// ...
}),
});
Mount rules:
- The path starts with
/and does not end with/unless it is exactly/. - Every agent id field appears as a
{variable}in the mount path. - Variable names use the exact TypeScript
idkeys, including casing. - Mount paths cannot contain query parameters or
{*rest}catch-all variables. - Use
{taskName}, not{task-name}, for a constructor field namedtaskName. This changes only the placeholder name; both forms would match the same concrete URL segment.
For a fresh phantom agent instance per HTTP request, set the mount option rather than changing the agent mode:
http: Http.mount("/gateway/{name}", { phantomAgent: true }),
mode still accepts only "durable" or "ephemeral"; it is independent of
phantomAgent.
Endpoint Declarations
Declare routes on the corresponding method. Endpoint paths are relative to the mount:
methods: {
listItems: method({
input: {},
success: Schema.Array(Item),
http: [Http.get("/items")],
}),
createItem: method({
input: {
name: Schema.String,
count: Schema.Number,
},
success: Item,
http: [Http.post("/items")],
}),
updateItem: method({
input: {
id: Schema.String,
name: Schema.String,
},
success: Schema.NullOr(Item),
http: [Http.put("/items/{id}")],
}),
deleteItem: method({
input: { id: Schema.String },
success: Schema.Void,
http: [Http.del("/items/{id}")],
}),
},
Use these exact helpers:
| HTTP route | Effect Golem helper |
|---|---|
| GET | Http.get(path, options?) |
| POST | Http.post(path, options?) |
| PUT | Http.put(path, options?) |
| DELETE | Http.del(path, options?) |
| Custom method | Http.custom("METHOD", path, options?) |
One method can have multiple routes by adding multiple entries to its http array.
Parameter Mapping
Bindings always refer to exact method input keys:
searchItems: method({
input: {
category: Schema.String,
query: Schema.String,
minPrice: Schema.NullOr(Schema.Number),
tenant: Schema.String,
},
success: Schema.Array(Item),
http: [
Http.get(
"/categories/{category}/search?q={query}&min-price={minPrice}",
{ headers: { "X-Tenant": "tenant" } as const },
),
],
}),
{category}binds a path segment toparams.category.q={query}binds query keyqtoparams.query; the URL key and TypeScript variable may have different names.- Missing optional query or header values decode to
nullwhen declared withSchema.NullOr(...). headersmaps HTTP header names to method parameter names.- A parameter can be bound from only one of path, query, or headers in a given endpoint.
Http.gethas no body, so every parameter must be bound explicitly.- For
post,put,del, and other bodyful endpoints, every unbound parameter comes from a JSON object field with the same TypeScript name. For example, unboundinStockexpects{ "inStock": true }.
Endpoint paths start with /. A {*rest} catch-all is allowed only as the final endpoint path
segment.
HTTP Response Mapping
The Golem HTTP host maps Effect method result schemas as follows:
| Method success schema | Handler success value | HTTP response |
|---|---|---|
Schema.Void |
Effect.void |
204, empty body |
T |
Effect<T> |
200, JSON T |
UnstructuredText(...) |
inline text reference | 200, plain text |
UnstructuredBinary(...) |
inline binary reference | 200, raw bytes |
Schema.NullOr(T) |
T |
200, JSON T |
Schema.NullOr(T) |
null |
404, empty body |
method with error: E |
Effect.fail(E) |
500, JSON E |
Schema.NullOr(T) is lowered by the Effect SDK to WIT option<T>, which is what enables the
host's 200/404 mapping. Use a declared error schema and Effect.fail(...) for expected typed
failures; do not use defects for normal not-found behavior.
Use an unstructured success value when the response itself must be plain text or binary rather than JSON:
import { Unstructured } from "@golemcloud/effect-golem";
const textSuccess = Unstructured.UnstructuredText({
restrictions: [{ languageCode: "en" }],
});
const binarySuccess = Unstructured.UnstructuredBinary({
restrictions: [{ mimeType: "application/octet-stream" }],
});
const textValue = {
_tag: "inline" as const,
val: {
data: "hello from Effect",
textType: { languageCode: "en" },
},
} satisfies Unstructured.TextReferenceValue;
const binaryValue = {
_tag: "inline" as const,
val: {
data: new Uint8Array([0, 127, 255]),
binaryType: { mimeType: "application/octet-stream" },
},
} satisfies Unstructured.BinaryReferenceValue;
Put textSuccess or binarySuccess in the method's success field and return
Effect.succeed(textValue) or Effect.succeed(binaryValue) from its handler. Inline text maps to a
text/plain body and uses textType.languageCode as Content-Language; inline binary maps to the
declared binaryType.mimeType. Unstructured successes cannot also declare a typed method error.
Use Schema.Struct(...), arrays, and other ordinary schemas for JSON responses, and Schema.Void
for a 204 empty response. These are fixed host mappings, not an arbitrary response builder.
Complete Durable Example
import { Effect, Ref, Schema } from "effect";
import { defineAgent, Http, method, Snapshot } from "@golemcloud/effect-golem";
const TodoItem = Schema.Struct({
id: Schema.String,
title: Schema.String,
done: Schema.Boolean,
});
const TodoState = Schema.Struct({
items: Schema.Array(TodoItem),
});
export const TodoAgent = defineAgent({
name: "TodoAgent",
mode: "durable",
id: {
listName: Schema.String,
},
http: Http.mount("/todos/{listName}"),
snapshotting: Snapshot.define({
schema: TodoState,
policy: Snapshot.policy.everyN(10),
}),
methods: {
createItem: method({
input: { title: Schema.String },
success: TodoItem,
http: [Http.post("/items")],
}),
listItems: method({
input: {},
success: Schema.Array(TodoItem),
http: [Http.get("/items")],
}),
completeItem: method({
input: { id: Schema.String },
success: Schema.NullOr(TodoItem),
http: [Http.post("/items/{id}/complete")],
}),
},
}).implement({
init: () => Ref.make({ items: [] as ReadonlyArray<typeof TodoItem.Type> }),
methods: (state) => ({
createItem: ({ title }) =>
Ref.modify(state, ({ items }) => {
const item = {
id: String(items.length + 1),
title,
done: false,
};
return [item, { items: [...items, item] }] as const;
}),
listItems: () => Ref.get(state).pipe(Effect.map(({ items }) => items)),
completeItem: ({ id }) =>
Ref.modify(state, ({ items }) => {
const existing = items.find((item) => item.id === id);
if (existing === undefined) return [null, { items }] as const;
const updated = { ...existing, done: true };
return [
updated,
{
items: items.map((item) => (item.id === id ? updated : item)),
},
] as const;
}),
}),
snapshot: Snapshot.ref<{ items: ReadonlyArray<typeof TodoItem.Type> }>(),
});
Register the top-level implementation:
// src/main.ts
import "./todo-agent.js";
Local imports use the emitted .js suffix in generated ESM projects.
Domain Deployment
Add the agent to the existing httpApi deployment without removing other agents:
httpApi:
deployments:
local:
- domain: my-app.localhost:9006
agents:
TodoAgent: {}
Current Golem 1.5 manifests contain deployment configuration only. Do not add legacy
apiDefinitions, route lists, OpenAPI extension bindings, or Rib response scripts. Route metadata
comes from Http.mount(...) and the method-level Http.*(...) declarations. Golem serves the
generated OpenAPI document at /openapi.yaml after deployment.
Key Constraints
- Import Effect APIs from
effectanddefineAgent,Http,method, andSnapshotfrom@golemcloud/effect-golem. - Do not use decorators or classes from
@golemcloud/golem-ts-sdkin an Effect component. - Every agent id field must appear in the mount path with exact TypeScript casing.
- Every bound path, query, or header variable must match a method parameter.
- Unbound bodyful-method parameters use same-named camelCase JSON body fields.
- Handlers return Effects, not plain values or
asyncfunctions. - Use
Snapshot.ref<Saved>()when a snapshottedRefcontains the saved schema value. - Import the implementation module from
src/main.ts; otherwise it is not registered. - Do not edit generated files under
golem-temp/.