name: golem-add-http-endpoint-moonbit description: "Exposing a MoonBit Golem agent over HTTP with mount paths and endpoint annotations. Use when the user asks to add HTTP endpoints, REST APIs, or web interfaces to an agent."
Adding HTTP Endpoints to a MoonBit Golem Agent
Overview
Golem agents can be exposed over HTTP using code-first route definitions. This involves:
- Adding
#derive.mount("/path/{param}")to the agent struct - Annotating methods with
#derive.endpoint(get="/path")(orpost,put,delete) - Adding an
httpApideployment section togolem.yaml(load thegolem-configure-api-domainskill)
Related Skills
| Skill | When to Load |
|---|---|
golem-add-agent-moonbit |
Creating a new agent from scratch before adding HTTP endpoints |
golem-http-params-moonbit |
Path/query/header variable mapping, body mapping, supported types, response mapping |
golem-configure-api-domain |
Setting up httpApi in golem.yaml, security schemes, domain deployments |
Steps
- Add
#derive.mount("/path/{param}")to the agent struct (below#derive.agent) - Add
#derive.endpoint(get="/...")(orpost,put,delete) to public methods - Optionally add
#derive.mount_auth(false)to disable authentication - Optionally add
#derive.mount_cors("https://origin.com")to configure CORS - Add
httpApideployment togolem.yaml(seegolem-configure-api-domainskill) - Build and deploy
Mount Path
The #derive.mount("/path") annotation on the agent struct defines the base HTTP path. Path variables in {braces} map to constructor parameters:
#derive.agent
#derive.mount("/api/tasks/{task_name}")
struct TaskAgent {
task_name : String
mut tasks : Array[TaskInfo]
}
fn TaskAgent::new(task_name : String) -> TaskAgent {
{ task_name, tasks: [] }
}
Rules:
- Path must start with
/ - Every constructor parameter must appear as a
{variable}in the mount path (using the parameter name) - Every
{variable}must match a constructor parameter name - Catch-all
{*rest}variables are not allowed in mount paths
Endpoint Annotation
The #derive.endpoint(...) attribute marks a public method as an HTTP endpoint. Specify one HTTP method with its path:
#derive.endpoint(get="/items")
pub fn TaskAgent::list_items(self : Self) -> Array[Item] {
self.items
}
#derive.endpoint(post="/items")
pub fn TaskAgent::create_item(self : Self, name : String, count : UInt64) -> Item {
let item = { id: self.items.length().to_string(), name, count }
self.items.push(item)
item
}
#derive.endpoint(put="/items/{id}")
pub fn TaskAgent::update_item(self : Self, id : String, name : String) -> Item {
// ...
}
#derive.endpoint(delete="/items/{id}")
pub fn TaskAgent::delete_item(self : Self, id : String) -> Unit {
// ...
}
Endpoint paths are relative to the mount path.
Query Parameters
Specified in the endpoint path using ?key={var} syntax:
#derive.endpoint(get="/search?q={query}&limit={max_results}")
pub fn MyAgent::search(self : Self, query : String, max_results : UInt64) -> Array[SearchResult] {
// query and max_results are extracted from the URL query string
}
Header Variables
Map HTTP headers to method parameters using #derive.endpoint_header("Header-Name", "param_name"):
#derive.endpoint(post="/data")
#derive.endpoint_header("X-Request-Id", "request_id")
pub fn MyAgent::submit_data(self : Self, request_id : String, payload : String) -> String {
// request_id comes from the X-Request-Id header, payload from the JSON body
}
Authentication
Disable authentication on a mount with #derive.mount_auth(false):
#derive.agent
#derive.mount("/public/{name}")
#derive.mount_auth(false)
struct PublicAgent {
name : String
}
CORS
Configure allowed CORS origins with #derive.mount_cors(...):
#derive.agent
#derive.mount("/api/{name}")
#derive.mount_cors("https://app.example.com", "https://other.example.com")
struct ApiAgent {
name : String
}
Multiple origins can be specified as separate string arguments.
POST Request Body Mapping
For POST/PUT/DELETE endpoints, method parameters not bound to path variables, query parameters, or headers are populated from the JSON request body:
#derive.endpoint(post="/items/{id}")
pub fn MyAgent::update_item(self : Self, id : String, name : String, count : UInt64) -> Item {
// id from path, name and count from JSON body: { "name": "Widget", "count": 5 }
}
Custom Types
All types used in endpoint parameters and return values must be annotated with #derive.golem_schema:
#derive.golem_schema
pub(all) struct Task {
id : String
title : String
done : Bool
} derive(ToJson, @json.FromJson)
#derive.golem_schema
pub(all) enum Priority {
Low
Medium
High
} derive(Eq, ToJson, @json.FromJson)
Return Type to HTTP Response Mapping
Golem maps method return types to HTTP status codes and response bodies according to the table below. This mapping is currently not configurable.
| Return Type | HTTP Status | Response Body |
|---|---|---|
Unit (no return) |
204 No Content | empty |
T (any type) |
200 OK | JSON-serialized T |
T? (Option[T]) |
200 OK if Some, 404 Not Found if None |
JSON T or empty |
Result[T, E] |
200 OK if Ok, 500 Internal Server Error if Err |
JSON T or JSON E |
Result[Unit, E] |
204 No Content if Ok, 500 if Err |
empty or JSON E |
UnstructuredBinary |
200 OK | Raw binary with Content-Type |
Complete Example
///|
/// Priority level for tasks
#derive.golem_schema
pub(all) enum Priority {
Low
Medium
High
} derive(Eq, ToJson, @json.FromJson)
///|
/// A task record
#derive.golem_schema
pub(all) struct Task {
id : String
title : String
priority : Priority
done : Bool
} derive(ToJson, @json.FromJson)
///|
/// A task management agent exposed over HTTP
#derive.agent
#derive.mount("/task-agents/{name}")
#derive.mount_auth(false)
struct TaskAgent {
name : String
mut tasks : Array[Task]
}
///|
fn TaskAgent::new(name : String) -> TaskAgent {
{ name, tasks: [] }
}
///|
/// List all tasks
#derive.endpoint(get="/tasks")
pub fn TaskAgent::get_tasks(self : Self) -> Array[Task] {
self.tasks
}
///|
/// Create a new task
#derive.endpoint(post="/tasks")
pub fn TaskAgent::create_task(self : Self, title : String, priority : Priority) -> Task {
let task = {
id: self.tasks.length().to_string(),
title,
priority,
done: false,
}
self.tasks.push(task)
task
}
///|
/// Get a task by ID
#derive.endpoint(get="/tasks/{id}")
pub fn TaskAgent::get_task(self : Self, id : String) -> Task? {
self.tasks.iter().find_first(fn(t) { t.id == id })
}
///|
/// Mark a task as complete
#derive.endpoint(post="/tasks/{id}/complete")
pub fn TaskAgent::complete_task(self : Self, id : String) -> Task? {
for t in self.tasks {
if t.id == id {
t.done = true
return Some(t)
}
}
None
}
# golem.yaml (add to existing file)
httpApi:
deployments:
local:
- domain: my-app.localhost:9006
agents:
TaskAgent: {}
Key Constraints
#derive.mount("/path")is required on the agent struct before any#derive.endpoint(...)annotations can be used- All constructor parameters must be provided via mount path variables
- Path/query/header variable names must exactly match method parameter names
- The endpoint path must start with
/ - Exactly one HTTP method must be specified per
#derive.endpoint(...)annotation - All custom types used in parameters or return values must have
#derive.golem_schema - Method names use
snake_case - Only
pub fnmethods can be exposed as HTTP endpoints - Never edit generated files —
golem_reexports.mbt,golem_agents.mbt, andgolem_derive.mbtare auto-generated bygolem build