Serve HTTP
Let's put a component on a real HTTP port. It takes two things: a rusm.toml that declares the listener and its routes, and the handler code itself. We'll serve GET / and GET /users/:id, then run it and curl it.
1. Declare the listener
A [[serve]] block is a port. A [serve.routes] table maps "METHOD /path" to "component#action" — one entry per route, any number of components, all in config. No router code. No middleware. RUSM dispatches each request to the right function and spawns a fresh sandboxed instance for it.
[[serve]]
protocol = "http"
listen = "127.0.0.1:8080"
[serve.routes] # "METHOD /path/:param" = "component#action"
"GET /" = "api#home" # → the `home` action in the `api` component
"GET /users/:id" = "users#show" # routed to a different component entirely
"POST /users" = "users#create" # same component, different action
"POST /webhooks" = "hooks#receive" # yet another component — one table, any mix
[components.api]
capability = "sandboxed"
[components.users]
capability = "sandboxed"
[components.hooks]
capability = "network-client" # components carry their own capability profileA path param is :name; a trailing * is a wildcard. Specificity wins (literal > param > wildcard); a matched path with the wrong method is 405, no match is 404.
TypeScript — export default { fetch } also works
If you have a single self-contained TS component that does its own routing, you can skip the routes table and point the listener at it directly with component = "api". It's the right call for a simple catch-all handler, but [serve.routes] is the preferred path for anything real — it's declarative, language-agnostic, and trivially extended.
2. Write the handler
// components/api/index.ts — one export per action in [serve.routes].
import { type Params } from "rusm-ts";
export function home(_req: Request, _p: Params): Response {
return new Response("Hello from RUSM 👋\n");
}
export function show(_req: Request, p: Params): Response {
return new Response(`user ${p.id}\n`);
}// components/api/src/lib.rs — one `pub fn` per action in [serve.routes].
// No main, no router — the macro hides the world, Guest, and export!.
use rusm_rs::http::{Params, Request, Response};
#[rusm_rs::handlers]
pub mod api {
use super::*;
pub fn home(_req: Request, _p: Params) -> Response {
Response::text("Hello from RUSM 👋\n")
}
pub fn show(_req: Request, p: Params) -> Response {
Response::text(format!("user {}\n", p.get("id").unwrap_or("?")))
}
}// components/api/main.go — register one handler per action in [serve.routes].
package main
import (
rusm "github.com/archan937/rusm/packages/rusm-go"
"github.com/archan937/rusm/packages/rusm-go/web"
)
func init() { rusm.Run(run) }
func main() {}
func run() {
h := web.NewHandlers()
h.Handle("home", func(_ web.Request, _ web.Params) web.Response {
return web.Text("Hello from RUSM 👋\n")
})
h.Handle("show", func(_ web.Request, p web.Params) web.Response {
id := p.Get("id")
if id == "" {
id = "?"
}
return web.Text("user " + id + "\n")
})
h.Serve()
}3. Build, serve, test
rusm build
rusm serve
# serving 1 endpoint(s): api → http://127.0.0.1:8080
curl http://127.0.0.1:8080/ # Hello from RUSM 👋
curl http://127.0.0.1:8080/users/42 # user 42Routing — the full picture
[serve.routes] is a single declarative table that wires your whole API. One listener, any number of routes, any number of components — no router code to write, no middleware to wire up.
[[serve]]
protocol = "http"
listen = "0.0.0.0:8080"
[serve.routes]
# static routes — exact match, fastest
"GET /" = "api#home"
"GET /healthz" = "api#health"
# path parameters — :name captures one segment, read from Params
"GET /users/:id" = "users#show"
"POST /users/:id" = "users#update"
# wildcard — trailing * captures the rest of the path
"GET /static/*" = "assets#serve"
# different components on the same listener — no problem
"POST /webhooks/gh" = "github#event"
"GET /admin/*" = "admin#handle"Specificity wins: a literal beats a param segment beats a wildcard — /users/me matches "GET /users/me" before "GET /users/:id". A matched path with the wrong method is 405 Method Not Allowed; no match at all is 404 Not Found. The component named in component#action is spawned fresh per request — so a crash drops one request, never the server, and there is no head-of-line blocking.
Multiple listeners get their own [serve.routes] table — public API on 8080, admin on 9090, each with its own routes and components:
[[serve]]
protocol = "http"
listen = "0.0.0.0:8080"
[serve.routes]
"GET /users/:id" = "api#show"
"POST /users" = "api#create"
[[serve]]
protocol = "http"
listen = "127.0.0.1:9090"
[serve.routes]
"GET /metrics" = "admin#metrics"
"POST /reload" = "admin#reload"No port shares a routes table with another — listeners are fully independent.
How it runs
Each request gets a fresh, sandboxed instance — spawned, it runs the action, replies, and is gone. Nothing is shared between requests, so a crash drops one request and never the server, and there's no head-of-line blocking. Because the instance is ephemeral, don't keep state in it — for that, reach a resident stateful service over the actor API, or persist to the node store (kv). For the execution model in full, see the serving model; for every [[serve]]/[serve.routes] field, the configuration reference.
Next: Serve WebSocket · Serve SSE.