Web Development
Geblang's web story supports APIs and server-rendered applications without trying to become a large full-stack framework.
The intended shape is Flask-like: a small set of routing, middleware, request, response, session, cache, and rendering primitives that framework authors can compose. Applications can use those primitives directly, and larger frameworks can layer conventions on top.
Native Web Module
import http;
import web;
let app = web.new();
web.before(app, func(dict<string, any> request): any {
return null;
});
web.get(app, "/users/:id", func(dict<string, any> request): dict<string, any> {
return web.jsonStatus({"id": request["params"]["id"]}, 200);
});
http.serve("127.0.0.1:8080", func(dict<string, any> request): dict<string, any> {
return web.handle(app, request);
});
Middleware registered with web.before can short-circuit before the route
handler. web.use and web.after transform responses after the handler.
Handlers receive a request dictionary and return a response dictionary, a
string, or null. Strings are normalized to 200 text responses and null is
normalized to 204 No Content. The request contains method, path, headers,
query/body data where available, and route parameters after routing. Response
helpers build dictionaries with status, headers, and body fields.
Before middleware receives (request) and should return null to continue or
a response-compatible value to stop the pipeline. Response middleware receives
(request, response) and should return the transformed response:
web.before(app, func(dict<string, any> request): any {
if (!request["headers"].hasKey("authorization")) {
return web.jsonStatus({"error": "missing token"}, 401);
}
return null;
});
web.use(app, func(dict<string, any> request, dict<string, any> response): dict<string, any> {
return web.withHeader(response, "X-App", "Geblang");
});
Source Web Modules
Source web modules are split by responsibility: web.http handles
request/response/context helpers, web.router handles routing and decorator
mounting, web.session handles sessions and flash messages, web.cache
handles cache stores, web.auth handles current-user helpers and CSRF,
web.validation wraps schema validation for request input, web.forms
provides SSR form helpers, web.middleware provides reusable middleware,
web.sse formats server-sent events, web.websocket handles WebSocket
upgrades and clients, and web.testing provides dispatch helpers for route
tests.
import web.http as wh;
import web.router as router;
let app = router.newRouter();
let api = router.group(app, "/api");
router.get(api, "/users/:id", func(dict<string, any> request): dict<string, any> {
let ctx = wh.context(request);
return wh.jsonStatus({"id": ctx.param("id")}, 200);
});
Route groups add a prefix without hiding the underlying router:
let admin = router.group(app, "/admin");
router.before(admin, auth.requireRole(sessionStore, "admin"));
router.get(admin, "/users", listUsers);
router.post(admin, "/users", createUser);
wh.context(request) provides a higher-level request wrapper for params,
query values, form data, cookies, sessions, rendering, and response helpers.
wh.requestObject(request) and wh.responseFrom(response) are useful when a
handler or middleware wants explicit request/response objects without adopting
the full context wrapper:
router.get(api, "/users/:id", func(dict<string, any> request): dict<string, any> {
let req = wh.requestObject(request);
return wh.responseObject(200, {"id": req.param("id")})
.header("X-Route", "users.show")
.toDict();
});
router.use(api, func(dict<string, any> request, dict<string, any> response): dict<string, any> {
if (wh.statusCode(response) >= 500) {
return wh.withHeader(response, "X-Error", "server");
}
return response;
});
Request Validation
web.validation wraps the lower-level schema module for common API and form
handlers. Validation results include valid, errors, and parsed data.
import web.validation as validation;
let userRules = validation.object({
"name": validation.stringField(),
"roles": validation.arrayOf(validation.stringField())
}, ["name"]);
router.post(api, "/users", func(dict<string, any> request): dict<string, any> {
let result = validation.json(request, userRules);
if (!validation.isValid(result)) {
return validation.errorResponse(result);
}
let data = validation.data(result);
return wh.jsonCreated({"name": data["name"]});
});
Use validation.form(request, rules) for SSR form posts and
validation.validate(data, rules) when the input is already parsed.
SSR Forms
web.forms builds on web.validation, web.auth, and web.session for
server-rendered form workflows:
import web.forms as forms;
import web.validation as validation;
let profileRules = validation.object({
"name": validation.stringField()
}, ["name"]);
router.post(app, "/profile", func(dict<string, any> request): dict<string, any> {
let result = forms.validate(request, profileRules);
if (!forms.isValid(result)) {
return wh.htmlStatus(forms.firstFieldError(result, "name"), 422);
}
return forms.redirectSuccess(sessionStore, request, "/profile", "Saved", {});
});
Use forms.csrfField(token) when rendering a hidden CSRF input,
forms.withCsrf(response, secret, options) to set the CSRF cookie, and
forms.verifyCsrf(request, secret) when handling the post.
Common Middleware
web.middleware provides reusable response middleware for the common HTTP
concerns that most apps need early:
import web.middleware as middleware;
router.use(app, middleware.securityHeaders());
router.use(app, middleware.requestId());
router.use(app, middleware.cors("https://example.com", "GET, POST", "Content-Type, Authorization"));
securityHeaders() adds conservative browser security headers,
requestId() propagates or creates an X-Request-ID response header, and
cors()/corsCredentials() add CORS headers. accessLog(logger) logs method,
path, and status through the log module after a response is produced.
Server-Sent Events
web.sse formats event-stream responses using the same response dictionary
shape as the rest of the web modules:
import web.sse as sse;
router.get(app, "/events", func(dict<string, any> request): dict<string, any> {
return sse.response([
sse.comment("ready"),
sse.event("user.created", "{\"id\":42}", {"id": "42"}),
sse.retry(5000)
]);
});
Use sse.streaming(handler) for long-lived event streams. The handler receives
an sse.EventStream and can write and flush SSE frames while the request stays
open:
router.get(app, "/live", func(dict<string, any> request): dict<string, any> {
return sse.streaming(func(sse.EventStream stream): void {
stream.write(sse.comment("connected"));
stream.flush();
});
});
WebSockets
WebSocket routes use the same router and response dictionary contract as HTTP
routes. web.websocket.upgrade(handler) produces an upgrade response; the
handler runs after the HTTP connection has become a WebSocket:
import web.websocket as ws;
router.get(app, "/ws", func(dict<string, any> request): dict<string, any> {
return ws.upgrade(func(ws.Connection conn): void {
let message = conn.readJson();
conn.sendJson({"echo": message["text"]});
conn.close();
});
});
The source wrapper also provides clients, so examples and integration tests can exercise both sides without dropping to the lower-level native module:
let conn = ws.connect("ws://127.0.0.1:8080/ws");
conn.sendJson({"text": "hello"});
let reply = conn.readJson();
conn.close();
Sessions, Auth, Cache
Server-side session stores are available for Redis, files, and SQL databases.
Cache stores follow a shared get, set, delete, has contract.
Auth helpers store the current user in the session and provide middleware guards:
router.before(api, auth.requireAuth(sessionStore));
router.before(admin, auth.requireRole(sessionStore, "admin"));
router.before(editor, auth.requirePermission(sessionStore, "posts.edit"));
The middleware helpers return callable guard values, so they can be passed directly
to router.before, decorator policy maps, or a framework layer. Lower-level
helpers such as auth.currentUser, auth.userHasRole, and
auth.userHasPermission remain available when custom guard logic needs more
than a single role or permission check.
Use file sessions for local apps and small deployments, Redis sessions when multiple app processes need shared state, and database sessions when the application already depends on SQL persistence and operational simplicity is more important than raw session throughput.
Flash messages and CSRF helpers are included for server-rendered forms:
let response = session.withFlash(sessionStore, wh.redirect("/settings"), request, "success", "Saved", {});
response = auth.withCsrf(response, secret, {});
SSR
SSR uses regular string/template helpers rather than a custom Geblang template language:
router.get(app, "/page", func(dict<string, any> request): dict<string, any> {
let ctx = wh.context(request);
return ctx.render("<h1>{{.title}}</h1>", {"title": "Geblang"});
});
For larger apps, keep templates as files and render them through the template
module or a thin application wrapper. Geblang deliberately avoids requiring a
custom template language for SSR.
Decorator Direction
Decorator-driven routing and middleware let framework code register handlers from metadata:
@route("GET", "/users/:id")
@loginRequired
func showUser(dict<string, any> request): dict<string, any> {
...
}
Framework code should scan decorator metadata with reflect and register
routes/middleware without new syntax.
The building blocks are:
- Decorators attach metadata to functions, methods, classes, and static methods.
reflect.decorators(value)andreflect.hasDecorator(value, name)expose that metadata.web.router.mount(router, controller)registers plain route metadata.web.router.mountWithOptions(router, controller, options)can map decorator metadata to middleware and policy guards.- Decorators such as
loginRequired,isGranted,requireRole, orrequirePermissioncan stay metadata-only and be interpreted by the router.
Example controller shape:
import web.http as wh;
import web.session as session;
let sessions = session.fileSessionStore("/tmp/app-sessions", 3600);
class UserController {
@route("GET", "/users/:id")
@loginRequired
@isGranted("admin")
func show(dict<string, any> request): dict<string, any> {
let ctx = wh.context(request);
return wh.jsonStatus({"id": ctx.param("id")}, 200);
}
}
let app = router.newRouter();
let api = router.group(app, "/api");
router.mountWithOptions(api, UserController(), {
"sessionStore": sessions
});
The goal is not to make decorators mandatory. Direct registration remains the lowest-level API and the best fit for small scripts.
mountWithOptions currently understands:
- Route decorators:
@route("GET", "/path"), plus verb aliases such as@get("/path"),@post("/path"),@put,@patch,@delete, and@optionswhere supported by the parser/runtime path in use. - Auth decorators with a
sessionStoreoption:@loginRequired,@requireAuth,@isGranted("role"),@requireRole("role"), and@requirePermission("permission"). - Named middleware decorators through the
middlewareoption map. - Class-level prefix decorators such as
@prefix("/api")are represented in metadata, but grouping withrouter.groupis the most explicit and portable option today.
For custom policy decorators with arguments, a framework layer can read
reflect.decorators directly and register the route however it wants. The
stdlib router keeps the built-in policy mapping intentionally small.
Practical App Layout
An application can keep HTTP wiring thin and put behavior in modules:
src/
main.gb
http/
routes.gb
controllers.gb
middleware.gb
domain/
users.gb
storage/
users_repository.gb
main.gb should create shared services, create the router, mount routes, and
start http.serve. Controllers should translate HTTP requests to domain calls
and return response dictionaries.