Web stack · Where a request lands
Astralis
The library for writing HTTP backends in Vala. An endpoint is a class, a route is metadata, a response is a value — and beneath it sit header and cookie handling, form parsing, server-sent events, a document builder and a layered JSON configuration system, all wired through Inversion and written with Invercargill.
- Version
0.1- Licence
- LGPL-3.0-or-later
- Package
usm install astralis- Language
- Vala
Overview
The server the stack stands on
Astralis takes an HTTP request from the socket to your code and back. It is built on GNU libmicrohttpd — driven in epoll mode from the GLib main loop, with suspend/resume so slow clients do not occupy a thread — and everything above that is Vala: routing by endpoint class, a pipeline of request values, results as values rather than writes to a socket.
The design is deliberately small. You implement the Endpoint interface, one async method that receives an HttpContext and returns an HttpResult:
class HelloEndpoint : Object, Endpoint {
public async HttpResult handle_request(HttpContext http_context, RouteContext route) throws Error {
return new HttpStringResult("Hello from Astralis!");
}
}
void main() {
var application = new WebApplication(8080);
application.add_endpoint<HelloEndpoint>(new EndpointRoute("/hello"));
application.run();
}
examples/SimpleApi.vala — a complete server.Routes can carry named segments, resolved for you into the route context:
class HelloNameEndpoint : Object, Endpoint {
public async HttpResult handle_request(HttpContext http_context, RouteContext route_info) throws Error {
string? name = null;
route_info.mapped_parameters.try_get("name", out name);
return new HttpStringResult(@"Hello, $(name ?? "Unknown")!");
}
}
// registered as:
application.add_endpoint<HelloNameEndpoint>(new EndpointRoute("/hello/{name}"));
examples/PathRouting.vala — the router's named-segment feature.Endpoints are registered into the Inversion container — scoped by default, so one instance serves one request — and the stack above builds on exactly this: Statum's pages, actions and channels are Astralis endpoints, and Spry's authentication rides inside them.
Features
What it hands you
- Endpoints and routing. Classes with route metadata; named segments like
/users/{id}/postsarrive pre-parsed in the route context. - Headers and cookies. Request headers and cookies as enumerable collections; response headers set fluently on the result value.
- Form data. An async
FormDataParseryields fields and files with typed getters and defaults for absent fields. - Server-sent events.
SseEndpointsubclasses broadcast to every open stream, greet new connections, and set a retry interval. - A document builder.
MarkupDocumentparses, builds and queries markup with XPath selectors — HTML without a template engine. - Response compression. gzip, brotli and zstd compressors with encoding negotiation;
FastResourceserves pre-loaded content straight from memory. - Layered configuration. A
web-config.jsonsystem with per-environment overlays merged over the base file.
Installation
Getting the library
With USM on your machine, one command acquires, builds and installs the library together with its C header, Vala binding and introspection:
usm install astralis
From source, the build lives at the repository root:
git clone <the astralis repository>
cd astralis
meson setup . build
ninja -C build
ninja -C build install
Then compile against it with Vala's usual package mechanism:
valac --pkg astralis-0.1 my-server.vala
astralis-0.1.deps resolves the Invercargill and Inversion bindings for you.Usage & documentation
Working with requests
Astralis has no README; its documentation is its examples. Seventeen runnable programs cover the API surface, and each idiom below is lifted from them.
Responses are values
An HttpResult carries body, status and headers together, so handlers stay pure and testable — headers chain onto the constructor:
return new HttpStringResult(user.to_json())
.set_header("Content-Type", "application/json")
.set_header("Access-Control-Allow-Origin", "*");
examples/JsonApi.vala and PathRouting.vala.Failures use the same vocabulary — a helper returns the status you name:
return error_response(@"Resource with ID $id not found", StatusCode.NOT_FOUND);
examples/ErrorHandling.vala.Headers, cookies and queries
Request headers and cookies arrive as Invercargill collections, so the whole Enumerable toolkit applies — count, iterate, filter — with single-field getters alongside:
var value = http_context.request.get_header(header_name);
http_context.request.cookies.to_immutable_buffer()
.iterate(kv => parts.add(@"$(kv.key): $(kv.value)"));
if (http_context.request.cookies.to_immutable_buffer().count() == 0) {
parts.add("(No cookies sent)");
}
examples/HeadersAndCookies.vala.Form bodies parse asynchronously, with getters that take defaults for absent fields:
FormData form_data = yield FormDataParser.parse(
http_context.request, upload_path);
var name = form_data.get_field_or_default("name", "Anonymous");
var email = form_data.get_field_or_default("email", "no-email@example.com");
var country = form_data.get_field_or_default("country", "us");
examples/FormData.vala — fields and uploaded files arrive together.Server-sent events
SseEndpoint is the base class for push. A singleton instance holds shared state; new_connection greets each client and broadcast_event reaches every open stream:
public class CounterEndpoint : SseEndpoint {
private int counter = 0;
private Mutex counter_mutex = Mutex();
public override uint retry_interval { get { return 2000; } }
public override async void new_connection(HttpContext http_context,
RouteContext route_context, SseStream stream) {
yield stream.send_event(
new SseEvent.with_type("counter", counter.to_string()));
}
public async void increment() {
counter++;
yield broadcast_event(
new SseEvent.with_type("counter", counter.to_string()));
}
}
examples/SseExample.vala — a mutex-guarded counter broadcast to all tabs.Building documents
When a handler must produce HTML rather than JSON, the markup subsystem parses, edits and queries documents without string templates:
var doc = new MarkupDocument.from_string(page_html);
var header_card = body.append_child_element("div");
header_card.append_child_with_text("h1", "A heading");
var toggle_form = item_div.append_child_element("form");
var toggle_btn = toggle_form.append_child_element("button");
examples/DocumentBuilder.vala — elements are built by appending children; XPath selectors find them again.MarkupTemplate composes documents, and XPath results come back as lazy Enumerables over the tree.
Static content and compression
FastResource holds content pre-loaded in memory — the example notes it should be registered as a startup or singleton since it keeps its bytes for the process lifetime:
FastResource.from_string(content) // load from a string
FastResource.from_byte_array(bytes) // load from bytes
FastResource(path) // load from the filesystem
examples/FastResources.vala — the three constructors.FilesystemResource serves a directory tree, and the compressor components negotiate gzip, brotli and zstd against what the client accepts — the file-server example announces exactly that:
Available compression: gzip, brotli, zstd
Try:
curl http://localhost:$port/
curl --compressed http://localhost:$port/style.css
examples/FileServer.vala, a complete static server in 141 lines.Configuration
Settings live in a web-config.json at the application root, and an environment overlay — web-config.staging.json, selected with ASTRALIS_ENV=staging — merges over it. Inside a handler, the configuration is injected through Inversion and read with typed getters:
var config = inject<WebConfig>();
var port = config.get_int("port", 8080);
var app_name = config.get_string("app_name", "Unknown App");
var debug = config.get_bool("debug", false);
examples/WebConfigExample.vala — every getter carries a default.Inside the box
The source tree names its own layers: Core holds WebApplication, the pipeline, HttpContext and the WebConfig family; Server holds the libmicrohttpd integration and socket monitor; Components holds routing and the three compressors; Endpoints holds the endpoint contracts and resource servers; Data holds form parsing; and Markup holds the document builder. The examples directory doubles as a guided tour of each.
Package & source
The ledger
- Version
0.1(fromMANIFEST.usm)- Licence
- LGPL-3.0-or-later — free to run, study, share and improve; the LGPL keeps it free to link from any program.
- Package
usm install astralis- Provides
libastralis-0.1.so,astralis-0.1.h,astralis-0.1.vapi+.deps,astralis-0.1.gir,astralis-0.1.typelib,astralis-0.1.pc- Needs at runtime
- GLib and GObject, libmicrohttpd, json-glib, libxml2, zlib, brotli and zstd encoders, plus the stack beneath:
libinvercargill-1.soandlibinversion-0.1.so. - Source
- The repository
Tilo15/astralison the Astrologue forge (git over ssh, port 222). Free software: the licence guarantees your right to the source.
The typelib ships as astralis-0.1.typelib, matching the GIR's namespace — an earlier package installed it under a lib-prefixed name that introspection consumers could never resolve; the mismatch is fixed and the manifest now names the file the build actually produces.