/examples/everyday-api

A small JSON API

A complete HTTP service in under fifty lines: one order type whose `json:` tags are the entire codec, a read handler, a stateful intake handler, and a router mounted on `std::http::Server`. Every section below concatenates to the exact program this build checked, and the one deliberate mistake shows the compiler catching the classic API bug — trusting a request body — at build time.

40 lines · verified by hale 0.19.2 at build time · download orders-api.hl

The type is the schema. `from_json` parses a document into an `Order` in one pass — a missing field is an error naming the field — and `to_json` writes it back. There is no separate schema language, no derive ceremony, and no map-of-anything passing through the handlers.

orders-api.hl
type Order {
    id: Int      `json:"id"`;
    item: String `json:"item"`;
    price: Int   `json:"px"`;
}

A route handler is an ordinary locus with a `handle` method — the interface is structural, so there is nothing to implement or register beyond the method itself. The path capture arrives through `ctx.params`, the parse failure is addressed inline with a default, and the response is a literal.

orders-api.hl
locus OrderRead {
    fn handle(ctx: std::http::Context) -> std::http::Response {
        let id = std::str::parse_int(std::http::path_param(ctx.params, "id")) or 0;
        let o = Order { id: id, item: "espresso", price: 350 };
        return std::http::Response {
            status: 200,
            body: Order::to_json(o),
            content_type: "application/json",
        };
    }
}

State is what a locus is for: `placed` is a param, private to the handler, no shared-mutability story required. The request body is attacker-controlled input, and `from_json` is fallible accordingly — the or-block turning a bad body into a 400 is not optional. Delete it and the build refuses:

orders-api.hl
locus OrderIntake {
    params { placed: Int = 0; }

    fn handle(ctx: std::http::Context) -> std::http::Response {
        let o = Order::from_json(ctx.req.body) or {
            return std::http::Response { status: 400, body: "not an order: " + err.field };
        };
        self.placed += 1;
        return std::http::Response { status: 201, body: Order::to_json(o) };
    }
}
Trust the request body.
-         let o = Order::from_json(ctx.req.body) or {
-             return std::http::Response { status: 400, body: "not an order: " + err.field };
-         };
+         let o = Order::from_json(ctx.req.body);
$ hale check orders-api.hl
orders-api.hl:23:17: type error: error not addressed: this expression's fallible result must be handled with an `or` clause (`or raise`, `or <fallback>`, `or handler(err)`) or a `match`
            let o = Order::from_json(ctx.req.body);
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The router is first-match-wins; the server is one statement. `max_accepts` bounds the accept loop, which is how a test drives the same server a deployment runs. Nothing on this page imported a framework — the server, router, JSON codec, and handler contract all ship in `std::http` and the language itself.

orders-api.hl
fn build_router() -> std::http::Router {
    let r = std::http::Router { };
    r.add("GET", "/orders/:id", OrderRead { });
    r.add("POST", "/orders", OrderIntake { });
    return r;
}

fn main() {
    std::http::Server { port: 8080, handler: build_router(), max_accepts: 2 };
}