Web and data
Fubby includes a web server, a database and background tasks. You don't import or install any of them.
Snow: web apps
A Snow app is ordinary Fubby. main registers routes at startup, then Snow serves requests. A handler is a function with no parameters and no return value, and you register it by name. Start an app with fubby snow serve, because fubby run doesn't start a server.
func main() {
let db = moon.db.sqlite.open("notes.db", {})
db.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, title TEXT NOT NULL)", [])
snow.get("/", "home")
snow.get("/hello/:name", "hello")
snow.get("/api/notes", "list_notes")
snow.post("/api/notes", "create_note")
snow.delete("/api/notes/:id", "delete_note")
snow.before("prepare")
snow.after("add_headers")
snow.on_error("failed")
}
func prepare() {
snow.request()["state"].set("app", "Notes")
}
func add_headers() {
snow.header("X-App", snow.request()["state"]["app"])
}
func failed() {
snow.json({"error": "Something went wrong"}, 500)
}
func home() {
snow.html("<h1>Notes</h1><p>See <a href=\"/api/notes\">the API</a>.</p>")
}
func hello() {
let name = snow.request()["params"]["name"]
snow.html(snow.template("<h1>Hello, {{name}}</h1>", {"name": name}))
}
func list_notes() {
let db = moon.db.sqlite.open("notes.db", {})
snow.json(db.query("SELECT id, title FROM notes ORDER BY id", []))
}
func create_note() {
try {
let title = json_decode(snow.request()["body"])["title"]
let db = moon.db.sqlite.open("notes.db", {})
let saved = db.execute("INSERT INTO notes (title) VALUES (?1)", [title])
snow.json({"id": saved["last_insert_rowid"], "title": title}, 201)
} catch (error) {
snow.json({"error": "Send JSON with a title"}, 400)
}
}
func delete_note() {
let id = snow.request()["params"]["id"]
let db = moon.db.sqlite.open("notes.db", {})
db.execute("DELETE FROM notes WHERE id = ?1", [id])
snow.text("", 204)
}Run it, and try the API from another terminal:
fubby snow serve notes.fab --port 8080
curl -X POST localhost:8080/api/notes -d '{"title": "First note"}'
curl localhost:8080/api/notes
Requests
snow.request() returns a map:
| Key | Value |
|---|---|
method | The HTTP method |
path | The decoded path, with no query string |
query | A map of query values. Repeated keys are rejected |
headers | A map, with lowercase header names |
params | Route parameters, such as id from /notes/:id, as strings |
body | The body as text. Decode JSON yourself with json_decode |
state | An empty map for your middleware and handlers to share during this request |
Responses
Use snow.html, snow.text or snow.json, each with an optional status code. snow.header sets a header, and snow.status sets the status. A handler must produce a response.
snow.template fills {{name}} placeholders, and escapes values for HTML. Use it for any value that comes from a user. It protects HTML text and quoted attributes, but not script, style or URL contexts.
Middleware and errors
snow.before handlers run in the order you registered them, and a response from one skips the rest and the route. snow.after handlers run afterwards, in reverse order. If a handler fails, the partial response is discarded, and the snow.on_error handler runs. snow.error() gives it the message, which you shouldn't show to users.
Static files
snow.static("/assets", "assets") serves a folder, resolved relative to your .fab file. Routes you register take priority, and folders are never listed.
Server options
| Option | Default | Range |
|---|---|---|
--host | 127.0.0.1 | An IP address, or localhost |
--port | 8080 | 0 to 65535 |
--workers | 1 | 1 to 8. The default handles one request at a time |
--queue-capacity | 16 | 1 to 64. When full, new connections get a 503 response |
--handler-timeout-ms | 1000 | 1 to 30000 |
--read-timeout-ms | 5000 | 1 to 30000 |
--write-timeout-ms | 5000 | 1 to 30000 |
--max-body-bytes | 65536 | Up to 1 MiB |
--max-response-bytes | 1048576 | Up to 10 MiB |
--max-requests | None | Stop after this many connections |
--shutdown-file | None | A file path that doesn't exist yet. Used to stop the server cleanly |
--quiet | Off | Turns off request logging |
To stop a server cleanly, start it with --shutdown-file /tmp/app.stop, then run fubby snow stop /tmp/app.stop. Ctrl-C also stops it, but it can interrupt a request.
Snow is for local development. It has no HTTPS, login, sessions, CSRF protection or rate limiting, and it doesn't support uploads through multipart forms. Keep it on your own machine.
Moon: SQLite
SQLite is built in. The second argument to open is a map of options, and an empty map is fine. Always pass values through ?1-style parameters, which keeps them separate from your SQL and prevents SQL injection.
func main() {
let db = moon.db.sqlite.open(":memory:", {})
db.execute("CREATE TABLE people (name TEXT, age INTEGER)", [])
let saved = db.execute("INSERT INTO people VALUES (?1, ?2)", ["Ada", 36])
print(saved)
db.execute("INSERT INTO people VALUES (?1, ?2)", ["Asha", null])
let rows = db.query("SELECT name, age FROM people ORDER BY name", [])
print(rows[0]["name"], rows[0]["age"])
print(rows[1]["age"] == null)
}{changes: 1, last_insert_rowid: 1}
Ada 36
trueUse a file name for a database that persists. Options are timeout_ms (default 1 second, maximum 30), max_rows (default 10,000, maximum 100,000) and max_bytes (default 1 MiB, maximum 10 MiB). SQL can't use ATTACH, DETACH or PRAGMA. SQL text is limited to 64 KiB, with up to 256 parameters.
Moon: HTTP
Network access is off by default. You allow specific hosts when you start the program, and a program can't change this itself. Redirects aren't followed, and proxy settings are ignored.
FUBBY_NET_ALLOWLIST=example.com,api.example.com fubby run app.fab
func main() {
if !moon.security.network_allowed("example.com") {
print("Start with FUBBY_NET_ALLOWLIST=example.com")
return
}
let response = moon.http.get("https://example.com/", {"timeout_ms": 3000})
print(response["status"], str_length(response["body"]))
let sent = moon.http.post(
"https://example.com/items",
json_encode({"name": "pen"}),
{"headers": {"content-type": "application/json"}},
)
print(sent["status"])
}Responses are maps with status, headers, body and url. Options are headers, timeout_ms (default 5 seconds, maximum 30) and max_bytes (default 1 MiB, maximum 10 MiB). Only GET and POST are available, and bodies must be UTF-8 text. moon.security.limits() returns every limit as a map.
Bela: background tasks
Bela runs named functions at the same time on a small pool of workers. Each task runs separately and shares no objects with its parent. Data passes between them as JSON, so a task gets a copy.
func square() {
let input = bela.input()
bela.result(input["value"] * input["value"])
}
func producer() {
let channel = bela.input()["channel"]
bela.send(channel, "ready")
}
func main() {
let task = bela.spawn("square", {"value": 12})
print(bela.join(task))
let channel = bela.channel(2)
bela.spawn("producer", {"channel": channel})
print(bela.recv(channel))
bela.close(channel)
}144
ready- A task function takes no parameters and returns nothing. It reads
bela.input()and setsbela.result(). - Workers can't start or join other tasks. Create channels in the parent, then pass them to tasks.
- Each task can be joined once.
joinraises the task's error if it failed. bela.cancelasks a task to stop, and takes effect between operations. It can't interrupt a slow operating-system call.bela.statusgivesqueued,running,cancelling,completed,failed,cancelled,timed_outorjoined.- When a program ends, any remaining tasks are cancelled and waited for.
The limits page lists Bela's bounds, and how to change them.