Skip to content

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 groups routes under a prefix. Nothing runs until you mount it on an app.

import { Router } from "@architectine/xil";
const orders = new Router("/orders");

.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.

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": "" }

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);
Terminal window
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": "..." }
  • The body was validated before the handler ran; a negative total never reached your code.
  • ctx.params.id and ctx.body.total were typed, not cast.
  • The thrown error and the validation failure rendered through the same envelope, with no error-handling code written by you.
  • app.express is a real Express app the whole time — anything in the Express ecosystem still works if you drop down to it.