Write a function
A function is an ordinary function in your source, plus one entry in your Driftfile naming it. Nothing in the code marks it — no decorator, no magic comment, no base class. Here's an endpoint that saves an item, in Go, with a validate helper beside it:
package main
import drift "github.com/ondrift/cloud/sdk/go"
// validate is a plain helper. The Driftfile never names it,
// so it never counts as one of your functions.
func validate(body map[string]any) bool {
return body["name"] != nil
}
func PostItems(body map[string]any, req drift.Request) (int, string, any, map[string]string) {
if !validate(body) {
return 400, "Bad Request", map[string]any{"error": "name required"}, nil
}
id, _ := drift.Backbone.NoSQL.Collection("items").Insert(body)
return 201, "Created", map[string]any{"id": id}, nil
}And the entry that turns PostItems into an endpoint:
atomic:
functions:
- route: items
method: post
handler: PostItemsA handler returns the status code, a short status message and the payload (serialised to JSON for you), plus an optional fourth value: a map of response headers. Leave it out when you don't need one — Go takes nil, and in Python, Node, Ruby and PHP a three-value return is complete on its own.
Rust is the one that sets headers differently. Its entry point destructures a fixed-size tuple, so a fourth value there would stop every existing handler compiling; call drift_sdk::set_response_header(name, value) instead and the return stays three values.
To return a body with no envelope, declare the shape.
{"status": …, "message": …, "payload": …}. Add response: to the function's Driftfile entry to change that — see What an entry declares below. Setting a Content-Type that isn't JSON also writes the payload straight to the wire, which is how this worked before response existed; it still does, but declare response: raw instead.Handlers that take a body (post, put, delete, patch and queue handlers) are called as (body, req). A get handler is called as (req).
Pay for what you expose, not what you write.
What an entry declares
Each entry in atomic.functions is the whole declaration for one function. Two fields are always required and the rest have defaults:
| Field | Meaning |
|---|---|
name | What the function answers on: post:items, get:groups/:id, or queue:orders for one the slice invokes from a queue. This is the identity Drift books, prices and meters. |
handler | The callable that serves it. |
memory | Its own pool, 8MB–256MB. No default — see Memory and concurrency. |
auth | none (default) or apikey. See Authentication. |
stream | sse or ws to hold the connection open. See Streaming. |
response | The shape of this function's replies: envelope (default), json or raw. See below. |
secrets | The Backbone secrets this one function may read. |
element | Which element it belongs to. Omitted means the flat files in atomic/. |
cron | A schedule it fires on in addition to its own trigger. See Triggers. |
The shape of a reply
Most endpoints answer other code you wrote, and the envelope is what the SDKs expect. An endpoint a third party parses usually cannot take it: a wallet, an OAuth client or a webhook sender wants your keys at the top level, in the shape its own spec names. response says which you are building.
- What reaches the caller
{"status": …, "message": …, "payload": …}- Content-Type
application/json
- What reaches the caller
- your payload, serialised — the whole body
- Content-Type
application/json, unless you set one
- What reaches the caller
- your payload as bytes, base64-decoded first
- Content-Type
- yours, and required
An OAuth token endpoint is the case this exists for — the spec pins it to application/json, which is also the envelope's own type, so nothing about the response itself can distinguish them:
- route: token
method: post
handler: PostToken
response: jsonasync function PostToken(body, req) {
return [200, "OK", { access_token: "…", token_type: "Bearer", expires_in: 3600 }];
}reaches the caller as {"access_token":"…","token_type":"Bearer","expires_in":3600} and nothing else.
Use raw for a body that isn't text at all — an image, a PDF, a signed token — and set your own Content-Type alongside it. Your payload is base64 there, because bytes have to survive a JSON hop to get to Drift.
How a handler is found
Drift looks for handler in the element's directory — atomic/ for the default element, atomic/<element> for a named one — and nowhere else. Two elements can each have a handle; within one element the name has to be unique.
The element's directory is also the whole of what gets deployed.
../dist/bundle.js, a sibling lib/, a config at the repo root — exists on your machine and is simply not there at runtime, which surfaces as a module-not-found or ENOENT in the deployed function rather than as a deploy error. Bundle what you need into the element, or give the shared code its own element.The callable has to be reachable from outside its own file, because the entry point Drift generates imports it by name:
| Language | Shape |
|---|---|
| Go | func PostItems(…), exported, capital first letter |
| Python | def post_items(…): |
| Node | function postItems(…), a named declaration; an arrow assigned to a const is not matched |
| Ruby | def post_items(…) |
| PHP | function postItems(…) |
| Rust | pub fn post_items(…), where pub is required |
The name is yours to choose. A handler the Driftfile names and the source doesn't have fails the deploy before anything is built, and the error lists the callables that directory does declare.
Node means JavaScript. Drift discovers .js, .mjs and .cjs; there is no TypeScript compiler in the deploy path, so a .ts file in an element is never looked at and its handlers are never found. Compile or bundle to JavaScript first — a pre_deploy hook is the usual place — and ship the output.
Routing
A function's address is the method and path from its name: post:items answers only POST /api/items. The method is part of the identity, not a detail. A request with a different method to the same path doesn't fall through to this function, it 404s. There's nothing to branch on inside the handler — you were routed here because you're the POST handler — so drift.Request deliberately doesn't expose the method.
So get:users (list) and post:users (create) are two functions with two handlers, two entries and two billing units. The path /users is a human grouping, not the unit; the unit is the function.
The /api space is shared across every element, so two entries declaring the same name collide wherever they live. drift file apply refuses the whole project and names both.
Method-and-path identity is why most CLI commands that address a single function take --method: without it, get:users and post:users are indistinguishable by name.