Stario 4 made the URL module config. 4.1 put a method on that location and called it Route. 4.2 finishes the sentence: a Route is the HTTP address. Method, host, and path. That is what you assign to a handler.
The address
A handler can have more than one address. Each Route is still one address. You write it once, register it with app.add, and build links from the same value.
The address can be static. It can also be a pattern — {room_id} in the path, {tenant} in the host. Those placeholders are part of the address, not a second language on top of it.
ROOMS = "/rooms"ROOM = ROOMS + "/{room_id}" LOBBY = Route("GET", ROOMS)SHOW = Route("GET", ROOM)SEND = Route("POST", ROOM + "/send") app.add(LOBBY, lobby)app.add(SHOW, room_page)app.add(SEND, send)ROOMS is a string prefix. SEND is the address. After a match, the captured values live on c.match. There is no other object that means “this endpoint.”
That is the focus. If the route is correct, the rest of the stack has something honest to work with.
Method is a string
Once the address includes the method, a factory per verb is extra type. HTTP methods have specs. They are still strings. GET and POST and QUERY and PROPFIND go in the same place: the first part of the address.
Route.get, Route.post, app.get, app.post, app.handle still work. They will go away in 5.0. Write Route("GET /home") or Route("POST", ROOM + "/send") and app.add(route, handler).
Prefixes stay strings
UrlPath was the type you composed and used as a prefix. Validation of a path is useful. Reinventing a type for every shared prefix is not. A common prefix is a string. You reuse it inside the Route.
API = "/api"USERS = Route("GET", API + "/users")app.use, not_found, Files, and Assets take the same kind of string. UrlPath still composes and still has href(). Prefer Route when you have an endpoint, and a / or // string when you only have a prefix.
The thing we should care about is whether the route is correct — not whether we invented another wrapper around a path.
at.fetch
I wanted at.fetch(route) to work. The route already knows the method, so the page could pick up the verb without repeating it. After using it, I dropped it.
Datastar’s @get and @post are not the same action with a different word. They differ by default and by design. If the method rides along from the Route without being named at the call site, a change on the server can change the browser action in a way you did not write down. That is implicit in the wrong place.
Build the URL with route.href(). Name the verb where the action is (at.get(...), at.post(...)). at.fetch stays until 5.0.
Assets and Files
stario.staticassets is still there. It is obsolete. The common cases are now two types.
Assets fingerprints names and 307s the logical path. That is CSS and JavaScript you want cached forever under a new URL when the bytes change.
Files keeps the name you gave it. Strong ETags, Last-Modified, and 304 when nothing changed. That is uploads, user media, anything that should stay at a stable URL.
Both take a directory and a prefix. Call href() at import. Call await attach(app) in bootstrap. You should not have to rebuild caching, ranges, and nosniff in every app.
stario.json
We encode and decode JSON in more than one place: responses, Datastar signals, telemetry, the test client. Python’s default JSON is correct and slow. The usual fix is to pick orjson or msgspec or ujson and thread it through the app.
A process-wide codec is not the most explicit design I have shipped. It is the one that actually addresses that slowness without making an extra library a requirement. The standard-library default stays. If the app already has a codec, set_codec() replaces it once. After that, the framework paths use the same dumps and loads. Little else changes for the reader of a handler.
Where to go
Routing —
Route, matching,c.matchStatic assets —
AssetsandFilesJSON — the process-wide codec
CHANGELOG.md— 4.2.0
If you only take one thing: the route is the address. Write it once. Assign it to the handler. Reuse the same value for links.
— Adam