Servers and Browser Apps

Refingere builds HTTP servers and browser apps with attributes on ordinary functions and structs, not with a framework. A route is a function that reads the request from &request and answers with a Response. A component is a struct with a render method. Both stay plain code, so tests call them directly. The complete programs are in HTTP Server and Webapp.

A server

@serve turns a function into a server: calling it runs its body, then listens on the port and answers requests until it's stopped. It's usually the @main function too. @get, @post, @put, @patch and @delete make a function the route for that method and path, and @route("OPTIONS", "/path") works for any other method. The server answers with every route in its folder and the folders inside it. A path no route matches gets a 404, and a path that matches with the wrong method a 405. See @serve.

What @serve stands for

@serve is a builtin attribute macro, and expanding it in place shows what it does. The function ends with #builtin.serve, which starts the JavaScript host's server, and gains a @js function beside it - here serverRequests - that the server calls with each request. That function hands the request to #builtin.answer, with every route by its method and path: it finds the route the request matches, puts the path's {name} segments in &request.params, calls the route with the request injected, and gives back its Response - or the 404, 405 or 500. A route in a folder inside the server's is named by its path, so it has to be global.

The request

A route takes no parameters: the request it's answering is &request, which the server injects before calling it. A {name} segment of the route's path is in &request.params, the query string in query, a submitted form's fields in form, the headers - named in lower case - in headers, and the whole body in body, along with its method and path. Every value is text, so the route parses what it needs, and decides itself what a missing or malformed one answers. See Request.

Responses

A route answers with a Response: a status (200 unless it says otherwise), headers and a body. Response.text, html, json, css and bytes make one with the Content-Type for their kind of body - #HTML, #CSS and #JSON make those values, with anything inserted through #{ } escaped for that language (see Macros). Built as a struct, a response can say anything: a status, any headers, a body. Without a Content-Type, a String body is sent as text and Bytes as raw bytes.

Statuses and errors

Every status is the route's own choice: the second argument of Response.text or json, a struct's status, Response.redirect - a 303, the usual ending for a form's POST - or Response.empty, a 204. A route that fails instead - an error it passes up with !! - answers 500, with the error's message as the body. So an answer the client should see, like a 404, is a Response, and an error is what went wrong in the server.

Shared state

State the routes share - a store, a database connection - is a const marked @injectLocally, which every call in the folder sees as &name. An inject in the @serve function's body doesn't reach the routes: it ends with the body, before the first request. Each request calls its route like any other call, so a route can inject values of its own for the code it calls. See Dependency Injection.

A database

#SQL {{{ ... }}} is a query: run() gives its rows as a Table, first() the first of them, and execute() how many rows a change changed. A query that selects the columns of an @entity struct's table gives that struct - getTodo's todo is a Todo?, and a column the struct doesn't have is an error when the program compiles. Any other query's rows are each a Map of its columns, unless it names its own row type: #SQL<Count> {{{ SELECT COUNT(*) AS n FROM todos }}}. @entity also writes the table's everyday functions beside the struct - Todo.migrate(), Todo.insert(title := "Milk"), Todo.select(id), Todo.update(id, done := true), Todo.delete(id), Todo.count() and more - so the queries above are only needed for what they don't cover. Each #{ } is a bound parameter - the value never becomes part of the statement, so it can't change what the query does. A query runs through &database, which the program injects - here an SQLite file, shared by every route as a const marked @injectLocally. It's made the first time something reads it. A failed query is an IOError, passed up with !! like any other, and without a database injected, running one is an IOError too.

A test injects its own database - SqliteDatabase.memory() is an empty one held in memory - so each test starts from nothing and leaves no file behind. The repository's examples/todo-server is a complete server built this way: a JSON API and a page, with its tests.

The server and SQLite come from the JavaScript host a program runs on - Bun, which --run and --test use (see JavaScript and TypeScript). A route's code is plain Refingere either way.

Testing routes

A route is an ordinary function, so a test injects the request it wants - Request's fields all have defaults, so it gives only the parts that matter - calls the route, and checks the Response. No server, no HTTP. Injecting the shared state gives each test its own.

A browser app

@app makes the folder a browser app rather than a program: once the page loads, the function is called, and the Html it returns replaces the content of the element mount selects. A folder has an @app or a @main, not both. In the browser, the context values come from the browser - &clock, &random, &logger - and there's no &fs. See @app.

The page imports the app like any other Refingere from a website, and rfn --web builds and serves it. load() puts the app on the page: there's nothing to call afterwards. Each component is rendered inside an element of its own, and a handler is found through an attribute naming it, so the page never runs code from the markup itself - and what toString() gives, in a test, has neither.

@component makes a struct a piece of the page with its own state. It has a render method returning Html, and is placed on the page by putting an instance into #HTML with #{ }. Fields marked @state can change, and setting one renders that component again - only that part of the page. Other fields are fixed once it's built, which is how a parent configures a child: Counter{step := 10}. An event handler is a lambda put into an attribute like onclick. Several changes in one handler cause a single render, after it returns.

Testing components

A component is a struct and its handlers are methods, so a test builds one, calls its methods, and reads what it renders with toString() - no browser needed.