REST¶
Expose HTTP endpoints (server) or call external REST APIs (client). The server connector is the most common way to create a Mycel microservice — it receives HTTP requests and triggers flows. The client connector calls external APIs as a step or target.
Server Configuration¶
connector "api" {
type = "rest"
port = 3000
cors {
origins = ["*"]
methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
headers = ["Content-Type", "Authorization"]
}
}
| Option | Type | Default | Description |
|---|---|---|---|
port |
int | — | Listen port |
cors.origins |
list | — | Allowed CORS origins |
cors.methods |
list | — | Allowed HTTP methods |
cors.headers |
list | — | Allowed headers |
The server speaks plain HTTP. There is no tls block on it and no certificate
to configure: terminate TLS in front of Mycel — an ingress, a load balancer, a
reverse proxy — which is where certificate renewal and cipher policy already
live in most deployments. The tls block below belongs to the client
half, and configures how Mycel verifies the servers it calls.
Client Configuration¶
connector "external_api" {
type = "http"
base_url = "https://api.example.com"
timeout = "30s"
auth {
type = "bearer" # "bearer", "api_key", "basic", "oauth2"
token = env("API_TOKEN")
}
retry {
attempts = 3
}
}
| Option | Type | Default | Description |
|---|---|---|---|
base_url |
string | — | Base URL for all requests |
timeout |
duration | "30s" |
Request timeout |
auth.type |
string | — | Auth method: bearer, api_key, basic, oauth2 |
retry.attempts |
int | 1 |
Maximum retry attempts. See HTTP Client for delay, max_delay and backoff. |
retry_count |
int | — | Shorthand for retry { attempts = N }. |
tls.ca_cert |
string | — | Path to a custom CA certificate (PEM) used to verify the server. |
tls.cert |
string | — | Path to client certificate (PEM) for mTLS. |
tls.key |
string | — | Path to client private key (PEM) for mTLS. |
tls.insecure_skip_verify |
bool | false |
Disable TLS certificate verification. Dev only — never use in production. |
TLS¶
For HTTPS endpoints whose certificate is signed by a private CA (e.g. an internal corporate CA, or a mkcert-signed dev proxy) point the connector at the CA bundle:
connector "internal_api" {
type = "http"
base_url = "https://internal.example.com"
tls {
ca_cert = "/etc/ssl/private-ca.pem"
}
}
For mutual TLS, add the client certificate pair:
For local development against a self-signed certificate (e.g. an nginx-proxy container in docker compose), skip verification entirely:
connector "magento" {
type = "http"
base_url = env("MAGENTO_BASE_URL")
tls {
insecure_skip_verify = true # dev only
}
}
When insecure_skip_verify is enabled, Mycel logs a single WARN at connector startup with the connector name and base URL — loud enough that an accidental production deploy is obvious in the logs.
The block is the same on every connector that speaks TLS — see TLS for the full attribute list and for the older client_cert / client_key names, which are still accepted.
Headers per request¶
The connector's headers block is sent on every request. A header whose value comes from the message — the store view, the tenant, the locale — goes on the step, to or enrich that makes the call (or on a saga's or state machine's action), as CEL expressions or constants, and wins over the connector's on the same name:
step "page" {
connector = "backend"
operation = "POST /graphql"
headers = { Store = "input.store", "X-Request-Source" = "mycel" }
body = { query = "'{ page(id: 1) { title } }'" }
}
A header that evaluates to null is not sent at all, rather than sent empty. A field the message does not carry is an error, as in any expression; write input.store ?? 'default' for a header with a fallback. The same attribute is honoured by the graphql client and soap connectors; on any other connector mycel validate refuses it, since nothing there would read it.
Wrapping the request body — envelope¶
Some REST frameworks (Magento webapi, Spring @RequestBody, several SOAP-derived REST APIs) require the request body nested under a single root key matching the service method's parameter name:
Rather than wrap the body inside a CEL map literal, set envelope on the to block. The transform stays clean — one line per attribute — and Mycel wraps the entire transform output under the named key just before it reaches the connector:
flow "magento_create_style" {
from {
connector = "rabbit"
target = "all.in.magento.q"
}
transform {
style_number = "input.body.payload.styleNumber"
name = "coalesce(input.body.payload.styleName, '')"
websites = "input.body.payload.websites"
# ...30 more lines, one mapping each
}
to {
connector = "magento"
target = "/rest/V1/mercury/products/styles"
operation = "POST"
envelope = "productData"
}
}
envelope also works on step blocks for intermediate HTTP calls that need the same shape. The wrap is a single key with the entire payload as its value — chained / nested wrappers are not supported (parenthesize manually with another transform if you need them).
Operations¶
Server (source): Any HTTP method + path pattern — GET /users, POST /users, PUT /users/:id, DELETE /users/:id. Path and query parameters arrive at the top of input for every method; a request body is decoded and merged there too for POST, PUT, PATCH, QUERY and DELETE (a body-less DELETE is the common case and stays as it is — before 3.6.2 a DELETE body was silently ignored). A body that does not parse is a 400.
Client (target): Same method + path syntax, resolved against base_url.
Where the data goes¶
POST, PUT, PATCH and QUERY carry their data in the body, and nothing else is added to the URL — a query string written into the target itself is kept, since that is what the flow asked for.
GET, DELETE and HEAD have no body, so their data is the query string. Values are rendered the way a query string can carry them: scalars as themselves, structured values as JSON, and a null omitted rather than sent. Parameters are sorted, so the same data always produces the same URL.
Fixed in 3.3.0
Before 3.3.0 a write appended its data to the URL for every method, including the ones that carry a body. For a flow writing to HTTP that data is the inbound message, so the whole message went out twice — once as the body that mattered, once as a query string nobody read. Small messages worked, so it stayed invisible until the request line passed a front-end proxy's limit and came back 414 Request-URI Too Large on an endpoint that accepted a large body without complaint. Structured values were also rendered with %v, arriving as Go's map[a:map[b:c]].
Debugging outbound requests¶
When the log level is set to debug (MYCEL_LOG_LEVEL=debug or --log-level=debug), the HTTP connector emits one log line per POST / PUT / PATCH request describing the body shape:
DEBUG outbound HTTP body connector=magento method=POST path=/rest/V1/products
size_bytes=27566 top_level_keys=[productData]
Only the top-level keys and total size are logged — no values, so it is safe to enable in environments where the body may contain sensitive data. Enough to verify wrap / envelope behavior end-to-end without intercepting traffic. The log is silent at any level above debug, so production logs are not noisier when you leave the default.
Example¶
flow "list_users" {
from {
connector = "api"
operation = "GET /users"
}
to {
connector = "db"
target = "users"
}
}
flow "create_user" {
from {
connector = "api"
operation = "POST /users"
}
transform {
id = "uuid()"
email = "lower(input.email)"
created_at = "now()"
}
to {
connector = "db"
target = "users"
}
}
See the basic example for a complete working setup.
File Upload (multipart/form-data)¶
The REST server connector auto-detects multipart/form-data requests and parses file uploads. The maximum upload size is 32MB.
Each uploaded file is encoded as a map with the following fields:
| Field | Type | Description |
|---|---|---|
filename |
string | Original file name |
content_type |
string | MIME type (e.g., image/png) |
size |
int | File size in bytes |
data |
string | File content encoded as base64 |
Files are available in transforms as input.files.<field_name>, where <field_name> matches the form field name used in the multipart request. Regular (non-file) form fields are available as input.<field_name>.
Example¶
flow "upload_avatar" {
from {
connector = "api"
operation = "POST /users/:id/avatar"
}
transform {
user_id = "input.id"
filename = "input.files.avatar.filename"
content_type = "input.files.avatar.content_type"
size = "input.files.avatar.size"
data = "input.files.avatar.data"
uploaded_at = "now()"
}
to {
connector = "db"
target = "user_avatars"
}
}
Upload with curl:
Full configuration reference: See REST Server in the Configuration Reference.