Your first app
This walks through the minimal example in the repository, one piece at a time. The full file is runnable as-is; see Examples to run it.
A router
Section titled “A router”A Router groups routes under a prefix. Nothing runs until you mount it on an app.
import { Router } from "@architectine/xil";
const orders = new Router("/orders");A route with validation
Section titled “A route with validation”.body(schema) accepts any Standard Schema value — a Zod object here. .handle() registers the route and gives you a typed ctx.
import { z } from "zod";
orders .post("/") .body(z.object({ total: z.number().positive() })) .handle((ctx) => { const order = { id: crypto.randomUUID(), total: ctx.body.total }; // ^ typed as number, already validated return order; // a plain object from a POST handler becomes a 201 JSON response });Nothing here mentions the response. Returning a plain value is enough; the router decides the status and content type from what you return, and from the HTTP method. See Responses.
An error
Section titled “An error”Throw. Never call next(err).
import { NotFound } from "@architectine/xil";
orders.get("/:id").handle((ctx) => { const order = findOrder(ctx.params.id); // ctx.params.id: string, inferred from ":id" if (!order) throw new NotFound(`Order ${ctx.params.id} does not exist`, { id: ctx.params.id }); return order;});Every error xil’s handler renders comes out the same shape:
{ "code": "NOT_FOUND", "message": "Order 42 does not exist", "details": { "id": "42" }, "requestId": "…" }Wiring it together
Section titled “Wiring it together”createApp builds a real Express application with body parsing, a request context, and an error handler already registered in the right order. app.mount() attaches a router at its own prefix.
import { createApp } from "@architectine/xil";
const app = createApp();app.mount(orders);
await app.listen(3000);Running it
Section titled “Running it”curl http://localhost:3000/orders/1# 404 with the unified error shape, since nothing has been created yet
curl -X POST http://localhost:3000/orders \ -H 'content-type: application/json' \ -d '{"total": 10}'# 201 { "id": "...", "total": 10 }
curl -X POST http://localhost:3000/orders \ -H 'content-type: application/json' \ -d '{"total": -5}'# 422 { "code": "VALIDATION", "message": "Invalid body", "details": { "in": "body", "issues": [...] }, "requestId": "..." }What just happened
Section titled “What just happened”- The body was validated before the handler ran; a negative total never reached your code.
ctx.params.idandctx.body.totalwere typed, not cast.- The thrown error and the validation failure rendered through the same envelope, with no error-handling code written by you.
app.expressis a real Express app the whole time — anything in the Express ecosystem still works if you drop down to it.
- How a request flows walks through what
createAppand the router actually do, in order, for every request. - Router covers path params, schemas, and typed narrowing in full.
- Using xil in an existing Express app if you are adding this to something that already exists.