Make Custom HTTP APIs for your Golem App
Golem's Gateway service not only exposes the low-level REST API for invocations but also provides a way to expose the agent methods as HTTP APIs.
Future versions will support other protocols such as MCP, gRPC, or GraphQL.
HTTP API definition
The HTTP APIs are defined in the application manifest (golem.yaml
) httpApi
section. Every template provided by Golem contains a sample API definition that can be used as a starting point.
The httpApi
section consists of two main parts:
- definitions contains the versioned API definitions describing routes and their handlers
- deployments specifies which sets of these definitions are exposed on which hosts
Definitions
Each API definition has a name and a version; multiple versions of the same API can be deployed simultaneously if needed, for example for backward compatibility.
The primary part of an API definition is the list of routes. Each route matches an incoming HTTP request and performs a specific action, most commonly invoking an agent method.
The following example defines an API called counter-api
with version 0.0.1
:
httpApi:
definitions:
counter-api:
version: '0.0.1'
routes:
# - method: GET
# ...
Routes calling agents
The most common type of route is one that invokes an agent method. A route consists of an HTTP method and path, with optional variables in it, and a Rib script. The script's job is invoking an agent's method by taking information from the request to assemble the agent ID and the method's parameters, and constructing an HTTP response from the method's response. This allows a lot of flexibility in mapping the agent to an HTTP API.
The routes
field is a list of routes, each consisting of:
method
: one of GET, POST, DELETE, PUT, PATCH, HEAD, CONNECT, OPTIONS, TRACEpath
: the path part of the URL, which may contain variables in{}
bracesbindings
: describes how to handle the request, for example by invoking an agent method
The bindings
field has several subfields:
type: default
: indicates that this route invokes an agent method; other types are documented belowcomponentName
: the name of the component containing the agent to invokeresponse
: the Rib script that handles the request- optionally
idempotencyKey
: a Rib script that computes an idempotency key for the request - optionally
invocationContext
: a Rib script that computes an invocation context for the request
To learn more about the Rib scripting language, see the Rib page.
Response script
The response script has access to the request
object, which allows access to details of the HTTP request:
request.path.xyz
refers to a path variable namedxyz
request.headers.xyz
refers to a header namedxyz
request.body
refers to the JSON request body - it can be inspected by accessing its fields such asrequest.body.exampleField
etc.request.auth
when authentication is configured (see below)
The script can construct an agent reference for any of the agent types defined in the component identified by componentName
by using the agent type name and providing the necessary constructor parameters. For example if there is an agent type counter-agent
with a single string constructor parameter, the following Rib snippets are constructing an instance of it:
let agent = counter-agent("test");
or
let agent = counter-agent(request.path.user);
This agent
variable then can be used to invoke an agent method, for example:
let new-value = agent.increment();
The script must return an object with at least a status
and a body
field:
{ status: 200u64, body: "Hello World" }
Additionally, this response object can also have a headers
field, which is a map of header names to values.
For example, it is possible to modify the response content type by adding a Content-Type
header:
{ status: 200u64, body: "Hello World", headers: { Content-Type: "text/plain" } }
The next example reads a custom header from the requests, and uses it as another custom header that is added to the response:
let customer-id: string = request.headers.X-Customer-ID;
{
status: 200u64,
body: "The customer id header was ${customer-id}",
headers: {
Content-Type: "text/plain",
X-Request-Customer-ID: customer-id
}
}
Example
The following example defines an increment endpoint for the example used on the Quickstart page:
httpApi:
definitions:
counter-api:
version: '0.0.2'
routes:
- method: POST
path: /{name}/increment
binding:
type: default
componentName: "example:counter"
response: |
let name: string = request.path.name;
let agent = counter-agent(name);
let new-value = agent.increment();
{ status: 200u64, body: { result: "incremented ${name}, new value is ${new-value}" } }
Note that when the content type header is not specified, the body
is automatically serialized as JSON and the Content-Type: application/json
header is added to the response.
Non-JSON request or response bodies
Currently Rib only supports JSON request bodies. The request.body
field is always a parsed JSON object, allowing access to its fields. Arbitrary payload can be only sent as part of the JSON object, for example by encoding it as a base64 string.
The response body's content type can be customized by adding a Content-Type
header to the response object, and it also depends on the request's Accept
header, if any. Depending on the accept and content type headers, different values returned in the response object's body
field have different outcomes:
- By default, if no
Accept
orContent-Type
header is present, the resulting content type isapplication/json
, and thebody
field can be any structured value, that is going to be serialized as JSON. - One exception to the above rule is if the value returned as the response body is an array of bytes. In this case the content type is set to
application/octet-stream
and the byte array is sent as-is in the response body. - If there is an explicit
Content-Type
header, or if theAccept
header in the request requires a specific content type, then the following rules apply:- If the content type is
application/json
, then thebody
field can be any structured value, that is going to be serialized as JSON. - If the content type is
text/plain
, then thebody
field must be a string, which is sent as-is in the response body. - If the content type is anything else, then the
body
field must be an array of bytes, which is sent as-is in the response body.
- If the content type is
Optional idempotency key script
In Golem every request is associated with an idempotency key. If there is no special idempotency key script provided, then this key is either taken from the Idempotency-Key
HTTP header, if any, or otherwise a random key is generated for each request.
The idempotencyKey
field of a binding can define a Rib script returning a string value. The following example uses a custom header instead of the Idempotency-Key
one for extracting the idempotency key:
request.headers.X-Custom-Idempotency-Key
Optional invocation context script
Every invocation in Golem has access to an invocation context. This invocation context has a span ID and a set of key-value pairs associated with it. Golem adds several keys by default to the invocation context, but by providing a Rib script in the binding's invocationContext
field, additional key-value pairs can be added to the invocation context of the agent method invocation based on the request.
The result of this Rib script must be an object of which each field is going to be added to the invocation context.
Authentication
Golem API gateway has built-in authentication support to identify users using OpenID protocol. See the API authentication page for details.
When authentication is configured for a route, the request
object in the Rib script has an additional auth
field, which contains the claims from the authenticated user's ID token. For example, if the authentication provider provides an email
claim, it can be accessed as request.auth.email
.
In the HTTP API route definitions, authentication is enabled by adding a security
field to the route (next to the method
, path
and binding
fields) referring to the preconfigured security scheme:
httpApi:
definitions:
counter-api:
version: '0.0.2'
routes:
- method: POST
path: /{name}/increment
security: my-security
binding:
type: default
componentName: "example:counter"
response: |
# ...
CORS
Setting up CORS for a route in API Gateway is done by specifying an extra route for the same path with OPTIONS
method and using the cors-preflight
binding type.
Optionally a response
field can be added to the binding, customizing the CORS headers:
httpApi:
definitions:
counter-api:
version: '0.0.2'
routes:
- method: POST
path: /{name}/increment
binding:
type: default
componentName: "example:counter"
response: |
# ...
- method: OPTIONS
path: /{name}/increment
binding:
type: cors-preflight
response: | # this field is optional
{
Access-Control-Allow-Origin: "*",
Access-Control-Allow-Methods: "GET, POST, PUT, DELETE, OPTIONS",
Access-Control-Allow-Headers: "Content-Type, Authorization",
Access-Control-Expose-Headers: "Content-Length, X-Requested-With",
Access-Control-Allow-Credentials: true,
Access-Control-Max-Age: 86400u64
}
File server bindings
The file-server
binding type can be used to retrieve files from an agent's filesystem. For read-write files the file is retrieved between running worker invocation, so users are guaranteed to not see any in-progress changes to files.
For read-only files the files are retrieved from a separate datastore and not directly from the agent. This makes retrieving read-only files very cheap and allows it be used for serving static content.
To learn more about the agent filesystem, check out the dedicated guide.
Example:
routes:
- method: Get
# {+var} patterns capture all remaining path segments
path: /workers/{worker-name}/files/{+file}
binding:
type: file-server
componentName: example-component
# retrieve the path /files/${{file}} from the worker and return the content.
response: 'let file: string = request.path.file; "/files/${{file}}"'
Parameters:
Parameter | Required | Type | Description |
---|---|---|---|
componentName | true | string | Name of the component to invoke |
workerName | false | rib | Script used to compute the name of the agent that will be invoked. The script has access to the request input. If not provided, an ephemeral worker is used. |
idempotencyKey | false | rib | Script used to compute the idempotency key. The script has access to the request input. If not provided an idempotency key will be generated. |
response | true | rib | Script that is used to compute the file to retrieve and the response to send to users. See the dedicatedc section for more details. |
File Server Response Object
The object returned from the response rib script warrants some extra mention. There are two different structures that are supported by golem:
-
Returning a single string. The returned string is interpreted as an absolute path and looked up in the worker filesystem and returned with an inferred content type and a 200 status code. If the file does not exist, a 404 is returned instead.
-
As an object. This object can have 3 different fields that are explained below:
Field Required Rib Type Description headers false record Headers that should be included in the response. status false u64 Status code of the response. If not provided defaults to 200. file-path true string Path of the file that should be retrieved from the worker filesystem. If the file does not exist a 404 is returned
Deployments
A deployment simply associates a set of API definitions with a host, for example to deploy the example API definition when running the local Golem server, the following deployment can be used:
httpApi:
definitions:
# ...
deployments:
local:
- host: localhost:9006
definitions:
- counter-api
Deploying the API
Once the API definition and deployments are added to the golem.yaml
file, the API can be deployed using the following command:
golem api deploy
The interactive shell will force you to update the version of the API definition to not lose track of the changes,
and keep a note that you can have advanced management of the APIs using the golem api definition
and golem api deploy
commands
allowing you to switch back to a previous API if needed.
Note that the api deploy
command also deploys the underlying components if needed.
Constraints
Once an API is deployed using a specific component-version, the agent methods used in the Rib script of the deployed API definition must be kept backward compatible. This is checked and enforced in the deploy command.
If there are conflicts the api deploy
command will fail with a conflict report message,
which details especially the missing functions and conflicting functions, where conflicting functions
detail about existing parameter types, new parameter types, existing return type, and new return type.
Exporting as OpenAPI specification
The API definitions can also be exported as OpenAPI specifications, which can be used for generating client code or as documentation.
Use the golem api definition export
CLI command to export the OpenAPI specification of a specific API definition:
golem api definition export --id counter-api --version 0.0.1
Troubleshooting
See the troubleshooting page for some common issues and their solutions.