Geblang web development

Build web applications with Gebweb

Gebweb is the recommended framework for REST APIs, backend services, and server-rendered applications in Geblang. It adds typed controllers, request binding, validation, dependency injection, OpenAPI, authentication, caching, templates, and production operations to the language's concurrent HTTP server.

Use the lower-level http module when you deliberately want a tiny handler or custom protocol adapter. For an application with routes, data models, middleware, tests, and operational requirements, start with Gebweb.

Controllers and validation

Create a typed JSON API

@Controller makes the class discoverable, so gebweb.app() mounts it without a manual controller list. A route decorator turns its method into an endpoint. A user class in the parameter list becomes the request body: Gebweb deserializes the JSON into that class and runs its @Assert constraints before the handler executes.

The valid request below returns 201. A one-character title never reaches the controller; it becomes a 422 Problem Details response with structured validation errors. Returning http.Response keeps status and headers explicit.

import gebweb;
import http;
import io;

class CreateTask {
    @Assert.minLength(2)
    string title;
}

@Controller
class TaskController extends gebweb.Controller {
    @Post("/tasks")
    func create(CreateTask body): http.Response {
        return this.json({"title": body.title}, 201);
    }
}

let app = gebweb.app();
let client = gebweb.TestClient(app);

let created = client.post("/tasks", {"title": "Ship release"});
io.println("${created.status}: ${created.json()["title"]}");

let invalid = client.post("/tasks", {"title": "x"});
io.println(invalid.status);

The program prints 201: Ship release and 422. For a real server, pass the same app to gebweb.cli(app, {"name": "tasks"}).

Continue with the Gebweb documentation for routing, typed parameter binding, and validation.

Services and persistence

Inject a database repository

@Service makes application services discoverable and @Controller does the same for controllers. Constructor types define the dependency graph, so gebweb.app() builds the graph automatically: the controller receives its repository and the repository receives the shared database service.

This self-contained example opens SQLite in the discovered database service. In a production service, the same singleton would create one PostgreSQL or MySQL db.Connection pool from environment-backed configuration and size it from measured request concurrency.

import gebweb;
import db;
import io;

@Service
class Database {
    db.Connection conn;

    func Database() {
        this.conn = db.Connection("sqlite", ":memory:");
        this.conn.exec("create table users (id integer, name text)");
        this.conn.exec(
            "insert into users (id, name) values (:id, :name)",
            {"id": 1, "name": "Mara"}
        );
    }
}

@Service
class UserRepository {
    Database database;

    func UserRepository(Database database) {
        this.database = database;
    }

    func name(int id): ?string {
        let rows = this.database.conn.query(
            "select name from users where id = :id",
            {"id": id}
        );
        defer rows.close();
        let row = rows.first();
        return row == null ? null : row["name"] as string;
    }
}

@Controller
class UserController {
    UserRepository users;

    func UserController(UserRepository users) {
        this.users = users;
    }

    @Get("/users/{id}")
    func show(int id): dict<string, any> {
        return {"name": this.users.name(id)};
    }
}

let app = gebweb.app();

let response = gebweb.TestClient(app).get("/users/1");
io.println(response.json()["name"]);

let database = gebweb.resolve(app, Database) as Database;
database.conn.close();

The path parameter is converted to int before invocation and the parameterized SQL value is kept separate from the query text. Read the dependency-injection guide and the Geblang database reference.

Fast application tests

Test the real dispatcher without opening a port

gebweb.TestClient sends requests through the same routing, middleware, binding, validation, authentication, cache, handler, and response pipeline used by the production server. Tests remain deterministic and fast because they do not bind a network socket.

The first API program above is already an end-to-end application test: it asserts behavior through HTTP-shaped requests rather than calling the controller directly. For wire behavior such as WebSocket upgrades or streaming SSE, start a real listener in the test and connect with the standard HTTP or WebSocket client.

The Gebweb testing guide covers cookies, headers, service stubs, response assertions, and the boundaries of the in-process client.

Concurrent requests

Keep shared services safe under load

Gebweb handles requests concurrently on goroutines. Controllers, default DI services, repositories, database pools, cache clients, and configuration are application singletons shared across requests. Request objects, handler locals, and services registered per request remain private to one request.

Do not mutate a plain shared dict, list, set, or object from several requests. Put shared in-process mutation behind store.Store or another synchronization primitive. Use PostgreSQL, MySQL, Redis, or another external system for state that must survive restarts or be shared by multiple application instances.

File sessions, memory caches, and the in-process WebSocket broadcast hub are single-process choices. Select distributed backends before horizontally scaling those features. See the Gebweb concurrency guide.

Operations

Add health checks, metrics, and admission control

useOps mounts liveness, readiness, and Prometheus metrics endpoints. Admission control bounds in-flight requests and returns a fast 503 when the measured capacity is full, preventing a traffic spike from turning into unbounded memory growth and latency.

Choose the concurrency limit from load tests that include the real database, cache, templates, and response sizes. Combine it with per-client rate limiting, downstream timeouts, and an edge proxy or load balancer.

import gebweb;
import io;

@Controller
class PingController {
    @Get("/ping")
    func ping(): dict<string, bool> {
        return {"ok": true};
    }
}

class AppProbes {
    @HealthCheck(name: "application", kind: "readiness")
    func ready(): bool {
        return true;
    }
}

let app = gebweb.app();
gebweb.useOps(app, [AppProbes()]);
gebweb.useAdmissionControl(app, {"maxConcurrent": 32});

let client = gebweb.TestClient(app);
io.println(client.get("/readyz").status);
io.println(client.get("/metrics").status);

Both checks print 200. In a deployed app, readiness probes should test required dependencies and return failure before a load balancer sends traffic to an unhealthy instance.

Build and deployment

Deploy one self-contained executable

Use gebweb.cli as the production entrypoint. It resolves ports and TLS from flags and environment variables, handles SIGINT and SIGTERM, marks readiness as draining, waits for in-flight requests, and then shuts the listener down.

gebweb build embeds the application, templates, public files, assets, dependencies, and Geblang runtime in one executable. Run it directly under systemd, in a container, or behind a reverse proxy.

gebweb build --out build/app
GEBWEB_PORT=8080 GEBWEB_ENV=production ./build/app

Terminate TLS at a trusted reverse proxy or configure Gebweb's native TLS, HTTP/2, mutual TLS, or automatic certificates. Persist databases and session storage outside the container, and expose health and metrics endpoints only to the systems that need them.

Lower-level option

When to use the standard HTTP module directly

Use http.serve directly for a tiny health process, webhook bridge, protocol experiment, or infrastructure component where framework routing and application services would add no value. The verified HTTP example shows the complete low-level server.

Choose Gebweb once the program needs several routes, typed request DTOs, validation, dependency injection, OpenAPI, auth, middleware, caching, templates, or production lifecycle conventions. That boundary keeps small programs small without forcing larger applications to reinvent a framework.