Writing a handler
A Drift function is a named handler with an @atomic annotation. The CLI reads the annotation and generates the program's entry point, in every language. You write the handler and nothing else. No main(), no run() call, no route registration.
- Annotation:
@atomic http=post:itemsfor an HTTP route, or@atomic queue=namefor a queue consumer. Path params use:name. - Arguments:
(req)for a request with no body (GET);(body, req)when there's a request body or queue payload. - Return:
(status, message, payload). Go returns a fourth value, a map of response headers; the other five return exactly three. - Request: read path params from
req.params, query fromreq.query, headers fromreq.headers, raw body fromreq.body. - Streaming: add
stream=sseorstream=wsand the handler receives an extraemit(SSE) orconn(WebSocket) argument.
// @atomic http=post:items auth=none
import drift "github.com/ondrift/cloud/sdk/go"
// Go only: the wrapper unmarshals the request body into this handler's
// own first-parameter type, read from your source. Each function in an
// element names its own body type, so two POST handlers never collide.
type PostItemsBody struct {
Name string `json:"name"`
}
func PostItems(body PostItemsBody, req drift.Request) (int, string, any, map[string]string) {
id, _ := drift.Backbone.NoSQL.Collection("items").Insert(body)
return 201, "Created", map[string]any{"id": id}, nil
}The annotation does the routing, so there is nothing to register and the handler's name is yours to choose, because the CLI reads it off the declaration the annotation sits above. The one-function-per-folder form (drift atomic deploy ./some-folder) is the exception: there the name is derived from the method and route, so http=post:items must be PostItems in Go and post_items in the snake_case languages. GET handlers take just (req).
In Go the body parameter can be any type you declare: the CLI reads the handler's signature out of your source and generates a wrapper that unmarshals into exactly that type. drift atomic new scaffolds <Handler>Body as a struct. Use map[string]any instead for a loose map. When the signature can't be read the wrapper falls back to a package-level RequestBody.
The request object
Every handler receives a req with the same fields in every language. The runner builds it from the inbound HTTP request and passes it to your function as JSON:
- Type
- string
- What it holds
- The request path, e.g.
/api/items/42.
- Type
- map
- What it holds
- Path params from the route's
:namesegments, sohttp=get:items/:id→req.params["id"]. Extracted for you.
- Type
- string
- What it holds
- Raw, URL-encoded query string (
"status=open&limit=50").not parsed. Split it with your language's URL library.
- Type
- map
- What it holds
- Request headers by name, as in
req.headers["Authorization"].
- Type
- any
- What it holds
- The parsed body: a JSON object for
application/json, or a form dict formultipart/form-dataandapplication/x-www-form-urlencoded(below). Also handed to you as the firstbodyargument.
- Type
- string
- What it holds
- The registered pattern that matched, e.g.
"items/:id". Set when a multi-handler element dispatches on it; empty otherwise.
Path params are extracted for you; the query string is not. A request to /api/orders?status=open arrives as req.query == "status=open", so parse it with url.ParseQuery (Go), URLSearchParams (Node), urllib.parse.parse_qs (Python), URI.decode_www_form (Ruby).
There is no req.method.
get:items and post:items are two separate functions. An off-method request 404s; it never reaches your code, so there is nothing to branch on.Form posts and file uploads
The runner parses multipart/form-data and application/x-www-form-urlencoded for you, so body arrives as a dict either way. No parsing library needed. A field that repeats becomes a list.
In a multipart post, text fields are strings and each uploaded file is an object with four keys:
- Type
- string
- What it holds
- The name the client sent, verbatim. Treat it as untrusted, and never join it into a path unsanitised.
- Type
- string
- What it holds
- The part's declared
Content-Type. Also client-supplied.
- Type
- string
- What it holds
- Standard base64, not raw bytes. Decode it before you store or inspect it.
- Type
- integer
- What it holds
- Length of the decoded bytes. Check it before decoding to reject an oversized upload cheaply.
data is base64 and no SDK decodes it for you.
blob.put stores the base64 text, not the file: roughly a third larger, and unreadable by anything that expects the original bytes. Use your language's own decoder: base64.b64decode (Python), Buffer.from(d, "base64") (Node), base64.StdEncoding.DecodeString (Go), Base64.strict_decode64 (Ruby), base64_decode (PHP).# @atomic http=post:upload auth=none
import base64
def post_upload(body, req):
name = body["applicant_name"] # a form field (string)
f = body["attachment"] # {filename, content_type, data, size}
if f["size"] > 5 * 1024 * 1024:
return 413, "Payload Too Large", {"error": "5MB maximum"}
raw = base64.b64decode(f["data"]) # decode BEFORE storing
drift.backbone.blob.put(f"uploads/{name}.pdf", raw, f["content_type"])
return 201, "Stored", {"name": f["filename"], "bytes": f["size"]}Return format
Every handler returns three values:
- Type
- integer
- Description
- HTTP status code (
200,201,400,500…)
- Type
- string
- Description
- Short status message, such as
"OK","Created"or"Bad Request"
- Type
- any
- Description
- Response body (objects and maps are serialised to JSON for you)
- Type
map[string]string- Description
- Response headers. Pass
nilfor none.
It's a tuple in Go, Python, and Rust; an array in Node.js, PHP, and Ruby.
The fourth value is Go's alone.
On the wire, the response body is an envelope.
{"status":…, "message":…, "payload":…}, not the bare payload. So a browser calling /api/<route> must read .payload (e.g. const data = (await res.json()).payload). The tuple's status also becomes the HTTP status code.There is one escape from the envelope, and it is Go-only for the same reason: a handler that sets a non-JSON Content-Type header has its payload sent as the raw body, base64-decoded, with no envelope. That is how you return a file, an image, or a page of HTML. It keys off a response header, and only Go can set one, so from the other five languages, serve files from Canvas or hand back a blob URL.
// @atomic http=get:report auth=none. Go: raw body, no envelope
func GetReport(req drift.Request) (int, string, any, map[string]string) {
pdf, err := drift.Backbone.Blob.Get("reports/latest.pdf")
if err != nil {
return 404, "Not Found", map[string]any{"error": "no report yet"}, nil
}
// payload must be a base64 STRING when Content-Type isn't JSON
b64 := base64.StdEncoding.EncodeToString(pdf)
return 200, "OK", b64, map[string]string{"Content-Type": "application/pdf"}
}The handler's own types
The CLI generates main() and calls the SDK's Run for you, in every language so you never call Run / run() yourself. A queue worker has the same shape as an HTTP handler; the queue message is delivered as the first body argument.
// HTTP handler: body present (POST/PUT) or omit it (GET): func GetX(req drift.Request)
func PostItems(body PostItemsBody, req drift.Request) (int, string, any, map[string]string)
// Queue worker: same shape; the message is `body`. Trigger: // @atomic queue=validate
func Validate(body map[string]any, req drift.Request) (int, string, any, map[string]string)
type Request struct {
// No Method field: you were routed here AS the post:items handler,
// so there's nothing to branch on. get:x and post:x are two functions.
Path string
Headers map[string]string
Query string // RAW query string, parse it yourself
Body json.RawMessage // also handed to you as the `body` arg
Params map[string]string // :name path params, extracted for you
RoutePattern string // the pattern that matched, for element dispatch
}
// Streaming args (stream=sse → emit, stream=ws → conn)
emit.Send(event string, data any) · conn.ReadJSON(target any) bool · conn.Write(data any)What the handler can call once it is running is the API reference; how the same handler looks in each of the six languages is Languages.