Getting started
Gebweb apps are built from controller classes: each method decorated
with a route annotation (@Get, @Post, ...) becomes an HTTP handler.
Mark a class @Controller and it is discovered and mounted
automatically. The DI container autowires class-typed constructor
dependencies; @Service also registers a class as a named service.
The framework wires every route through a binding adapter and mounts SwaggerUI at /docs plus
the OpenAPI spec at /openapi.json.
Install Gebweb
Install the Geblang toolchain first. Gebweb has two parts: the framework package that your program imports and an optional CLI for creating, developing, and building projects.
Download the gebweb CLI for your platform from the
Gebweb releases, extract
it, and put the binary on your PATH. If you have Go installed, you can build
the CLI instead:
go install github.com/dwgebler/gebweb/cmd/[email protected]
Keep both geblang and gebweb on your PATH. Create a project, then install
the framework package inside it:
gebweb new myapp
cd myapp
geblang install github.com/dwgebler/[email protected]
gebweb dev
The wizard writes geblang.yaml and a runnable src/main.gb; the package
install lets import gebweb; resolve. Run geblang test src/ for the generated
tests. In an existing Geblang project, run just the geblang install command
from its root. Use @latest instead of a version tag to select the newest
release. The CLI is optional when you run applications directly with Geblang.
A first API
This small notes API shows the shape of a real Gebweb app: a service
holds the state, a controller exposes it, and nothing is registered by
hand. gebweb.app() is called with no controller list - the framework
discovers NotesController (it carries @Controller) and autowires
NoteService (it carries @Service) into its constructor.
import gebweb;
import http;
@Service
class NoteService {
dict<string, dict<string, any>> notes;
int counter;
func NoteService() { this.notes = {}; this.counter = 0; }
func add(string text): dict<string, any> {
this.counter = this.counter + 1;
let note = {"id": "n-${this.counter}", "text": text};
this.notes[note["id"] as string] = note;
return note;
}
func all(): list<dict<string, any>> {
let out = [];
for (id in this.notes.keys()) { out = out.push(this.notes[id]); }
return out;
}
func find(string id): ?dict<string, any> {
if (this.notes.contains(id)) { return this.notes[id]; }
return null;
}
}
class NoteInput {
string text;
}
@Controller("/notes")
class NotesController extends gebweb.Controller {
NoteService notes;
func NotesController(NoteService notes) {
this.notes = notes;
}
@Get("")
@Summary("List notes")
func index(): http.Response {
return this.json(this.notes.all());
}
@Post("")
@Summary("Create a note")
func create(NoteInput body): http.Response {
return this.json(this.notes.add(body.text), 201);
}
@Get("/{id}")
@Summary("Fetch one note")
func show(string id): http.Response {
let note = this.notes.find(id);
if (note == null) { throw gebweb.notFound("no note " + id); }
return this.json(note);
}
}
let app = gebweb.app();
gebweb.serve(app, "127.0.0.1:8080");
POST /notes with {"text": "buy milk"} returns 201 and the created
note; GET /notes/n-1 returns it; GET /notes/missing returns a 404
RFC 9457 problem document. Visit /docs for the auto-generated
SwaggerUI page describing every route.
Request lifecycle
A request flows through:
- Routing.
stdlib/web/routermatches method + path against the registered route table and resolves path parameters. - Middleware (before). Any request-phase callable registered via
gebweb.beforeruns. Returning a response dict short-circuits. - Cache lookup. If the matched handler carries
@Cache, the framework consults the registered cache store and returns the cached response on a hit. - Authentication. If the handler (or its enclosing controller
class) carries
@Author@RequiresRole, the registered authenticator runs. Failure becomes a 401 / 403. - Parameter binding. Path / query / body / file parameters are coerced into the handler's typed parameter list.
- Validation. Any DTO bound from the request body is run
through the registered
@Assert.*validators. - Handler. Your method runs.
- Response shaping. Dicts and lists become JSON; pre-shaped
response dicts pass through;
gebweb.stream/@Sse/@WebSocketare recognised by the lower-level dispatcher. - Middleware (after). Response-phase middleware (e.g.
cors,securityHeaders,compress) transforms the response.
App construction
gebweb.app() creates the app and loads config/services.yaml when that file
exists. With no controller list, Gebweb mounts every loaded class marked
@Controller, including classes with a path prefix such as
@Controller("/notes") in the first API. Import controller modules before
serving so their classes are loaded and visible to discovery.
Discovery runs once when the app first handles requests through serve,
listen, TestClient, or dispatcher. You can call gebweb.discover(app)
earlier to inspect its DiscoveryReport; calling it again does not register
anything twice. Alongside controllers, the sweep finds @Service classes,
@Policy methods, and tagged event, job, message, and scheduled handlers.
The DI container creates discovered controllers and their constructor
dependencies. For the first API, it sees NotesController(NoteService notes),
constructs a NoteService, and passes it to the controller. It recursively
resolves class-typed constructor parameters even when the dependency class has
no @Service marker. @Service makes a class discoverable as a named service
and available for service ids and tags; it does not need a factory for a class
with a resolvable constructor. Register values that cannot be constructed from
their types, such as an existing database connection, before the first
request. See Dependency injection for factories,
instances, configuration parameters, interfaces, and lifetimes.
If you pass a nonempty list to gebweb.app(controllers), that list is the
complete controller set: other @Controller classes are not auto-mounted.
Services, policies, and tagged handlers are still discovered. The list accepts:
- An already-constructed controller instance
(
gebweb.app([HelloController()])). - A controller class reference; the DI container instantiates it
with autowired dependencies (
gebweb.app([HelloController])). - A class decorated with
@ApiResource; the framework generates auto-CRUD routes (see Repositories).
Use an explicit list when you need to limit which controllers are mounted, or
when a controller has no @Controller marker. Calling gebweb.app([]) behaves
like gebweb.app() and keeps automatic controller discovery enabled.
gebweb.setInfo(app, info) overrides the OpenAPI info object:
let app = gebweb.setInfo(gebweb.app(), {
"title": "Notes",
"version": "0.1.0",
"description": "A minimal Gebweb application.",
});
The /openapi.json (spec) and /docs (SwaggerUI) routes mount automatically.
An explicit gebweb.app option turns them off and always overrides the
environment: {"docs": false} disables both, {"swaggerUi": false} keeps the
spec but drops the UI, and {"openapi": false} disables both (the UI needs the
spec). GEBWEB_DOCS=off or GEBWEB_ENV=production disable them via the
environment; gebweb.useDocsAuth(app, guard) keeps them mounted behind auth.
Serving
gebweb.cli(app, opts)- the standard entrypoint for deployable apps. Resolves ports and TLS from command-line flags,GEBWEB_*environment variables, and theoptsdefaults (in that precedence), prints a "serving http://... - Ctrl+C to stop" banner, and shuts down cleanly on SIGINT / SIGTERM. Flags:--port,--tls-port,--host,--domain(LetsEncrypt autocert with an HTTP redirect listener),--self-signed(local HTTPS with a generated certificate),--no-tls,--acme-email,--acme-cache,--trusted-proxies,--help.opts.serverpasseshttp.listenoptions (such astrustedProxiesormaxConns) to every listener.
gebweb.cli(app, {"name": "myapp", "port": 8080});
# full option/precedence/TLS/drain reference: docs/37-deployment.md
# ./myapp --port 9000
# ./myapp --self-signed # https://localhost:443, generated cert
# ./myapp --domain example.com # LetsEncrypt on :443, redirect on :80
gebweb.serve(app, addr)- blocking, address-only. Use when the operational surface is managed elsewhere.gebweb.listen(app, addr)- non-blocking. Returns anintlisten handle suitable forhttp.shutdown(handle).gebweb.dispatcher(app)- returns the in-processcallable(request) : responsefor direct invocation (used byTestClient).gebweb.routes(app)- returns the registered route table for introspection / debugging.
To serve several ports at once, call gebweb.listen for each (it is
non-blocking) and http.wait on a handle to keep the process alive - for
example plain HTTP and self-signed HTTPS from the same app:
gebweb.listen(app, "127.0.0.1:8080");
let server = gebweb.listen(app, "127.0.0.1:8443", {"tls": {"selfSigned": true}});
http.wait(server);
Reference
gebweb.app(list<any> controllers = [], ?dict<string, any> opts = null): GebwebAppgebweb.setInfo(GebwebApp app, dict<string, any> info): GebwebAppgebweb.cli(GebwebApp app, dict<string, any> opts = {}): voidgebweb.serve(GebwebApp app, string address, ?dict<string, any> opts = null): voidgebweb.listen(GebwebApp app, string address, ?dict<string, any> opts = null): intgebweb.dispatcher(GebwebApp app): callablegebweb.routes(GebwebApp app): list<dict<string, any>>