Skip to content

HTTP

HTTP handlers are declared in a service inside a context. See the grammar for HTTP handlers for the production and the diagnostics that govern it.

service <Name> from http {
on <METHOD>("<route>") (<params>) -> Effect[HttpResult[T]] {
}
}
  • Methods: GET, POST, PUT, PATCH, DELETE.
  • Route: must start with /; a :name segment is a path parameter.
  • Parameters: each parameter is either a path parameter (matching a :name segment) or the special body parameter. A path parameter’s type must be constructible from a string (bynk.http.path_param_not_stringy); GET and DELETE may not take a body (bynk.http.body_on_get_or_delete).
  • Return type: must be Effect[HttpResult[T]] (bynk.http.return_not_effect_http_result).

A body parameter is parsed from the request JSON and validated before the handler runs; an invalid body is rejected with 400 at the boundary.

The vocabulary tracks the common, modern HTTP status codes (RFC 9110). A variant’s payload is one of five shapes: the value T as JSON (Value), a target URL emitted as a Location header (Location), an explanatory message as an { "error": … } JSON body (Message), a Stream[String] emitted as an SSE (text/event-stream) body (Streamed), or no body at all (None).

VariantStatusPayload
Ok(value)200the value, as JSON
Streaming(stream)200a Stream[String], SSE-framed (see Streamed responses)
Created(value)201the value, as JSON
Accepted(value)202the value, as JSON
NoContent204none

A redirect carries the target URL, emitted as a Location header with an empty body.

VariantStatusPayload
MovedPermanently(url)301Location header
Found(url)302Location header
SeeOther(url)303Location header
TemporaryRedirect(url)307Location header
PermanentRedirect(url)308Location header
VariantStatusPayload
BadRequest(message)400message
Unauthorized401none
Forbidden403none
NotFound404none
MethodNotAllowed405none
NotAcceptable406none
RequestTimeout408none
Conflict(message)409message
Gone410none
LengthRequired411none
PayloadTooLarge(message)413message
UnsupportedMediaType(message)415message
UnprocessableEntity(message)422message
TooManyRequests(message)429message
UnavailableForLegalReasons(message)451message
VariantStatusPayload
ServerError(message)500message
NotImplemented(message)501message
BadGateway(message)502message
ServiceUnavailable(message)503message
GatewayTimeout(message)504message

Streaming(stream) returns a 200 whose body is a Stream[String], emitted as Server-Sent Events (content-type: text/event-stream). Each stream element becomes one SSE event — data: <element>\n\n — so a handler can send an incremental feed without buffering the whole response:

on GET("/ticks") by v: Visitor () -> Effect[HttpResult[()]] {
Streaming(Stream.of(["tick-1", "tick-2", "tick-3"]).take(3))
}

A streamed handler returns Effect[HttpResult[()]] — the JSON body parameter T is unused, since the body is the stream. A response commits its status and headers before the first chunk, so streaming is 200-only: handle a pre-stream failure by returning an ordinary variant instead of Streaming (NotFound, Unauthorized(…), …), which share HttpResult[()] and so sit in the same handler with no type conflict:

on GET("/feed/:mode") by v: Visitor (mode: String) -> Effect[HttpResult[()]] {
if mode == "live" {
Streaming(Stream.of(events).take(100))
} else {
NotFound
}
}

A producer that can fail mid-stream carries its outcome in-band — build a Stream[Result[String, E]] and .map it to Stream[String], encoding an Err as an error event — because the HTTP status is already sent once streaming begins. A bounded take is the language-level guard against an unbounded response. A structured event type (named event/id/retry fields) is a planned follow-on; v1 streams plain String events.

no

yes

no

yes

incoming request

Worker fetch — index.ts router

route matches?

404

bind :name path params

body valid?

400 at the boundary

handler runs — returns Effect

HttpResult[T]

HTTP status + JSON body

Validation happens once, at the edge; the handler only ever sees valid input.

Text equivalent: the Worker’s fetch entry point (index.ts) routes the request; an unmatched route is a 404. On a match, path parameters are bound and any body is parsed and validated against its refined type — an invalid body is rejected with 400 at the boundary, before the handler runs. The handler then runs as an Effect and returns an HttpResult[T], which is mapped to an HTTP status and JSON body per the table above.

context notes
service api from http {
on GET("/ping") by Visitor () -> Effect[HttpResult[String]] {
Ok("pong")
}
on GET("/notes/:id") by Visitor (id: String) -> Effect[HttpResult[String]] {
NotFound
}
}

from http services compile to a runnable Cloudflare Worker on the --target workers target (index.ts router, handlers.ts, compose.ts, wrangler.toml). See emission and Target Cloudflare Workers.