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.
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 step, and no map-of-anything passing through the handlers.
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 the method itself is all there is to implement or register. The path capture arrives through ctx.params, the parse failure is addressed inline with a default, and the response is a literal.
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, with no shared mutable state to manage. The request body is input an attacker controls, and from_json is fallible accordingly, so the or-block turning a bad body into a 400 is required. Delete it and the build refuses:
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: `Order::from_json` can fail (JsonError) and this call says nothing about it: write `or raise` to hand the failure to the caller, `or <fallback>` for a value to use instead, `or handler(err)` to deal with it here, or `or discard` when losing it is the intent. A bare call to a fallible entry point is an error since v0.22.0 (GH #738).
let o = Order::from_json(ctx.req.body);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^The router keeps what add hands it as a borrow, so each handler is a field of the locus that owns the router and lives exactly as long as it does. The router takes the first match, and the server is one statement that mounts that owner. max_accepts bounds the accept loop, which is how a test drives the same server a deployment runs. The page imports no framework: the server, router, JSON codec and handler contract all ship in std::http and the language itself.
locus Routes {
params {
read: OrderRead = OrderRead { };
intake: OrderIntake = OrderIntake { };
router: std::http::Router = std::http::Router { };
}
birth() {
self.router.add("GET", "/orders/:id", self.read);
self.router.add("POST", "/orders", self.intake);
}
fn handle(req: std::http::Request) -> std::http::Response {
return self.router.dispatch(req);
}
}
fn main() {
std::http::Server { port: 8080, handler: Routes { }, max_accepts: 2 };
}