httpserver¶
The httpserver package provides a production-grade HTTP server with routing, middleware pipelines, static file serving, cookies, CORS, and Tomcat-style configuration.
Quick Start¶
import "httpserver"
server = httpserver::create(8080)
server.get("/", with (req, res) do
res.text("Hello, World!")
end)
server.get("/greet/:name", with (req, res) do
res.json({ "message": "Hello, " + req.param("name") + "!" })
end)
server.start()
Factory Functions¶
create(port?, host?)¶
Creates a new Server with default configuration.
Parameters
| Type | Name | Description | Default |
|---|---|---|---|
integer |
port |
Port to listen on | 8080 |
string |
host |
Bind address | "localhost" |
Returns Server
create_with_config(cfg)¶
Creates a new Server from a Config object.
Parameters
| Type | Name | Description |
|---|---|---|
Config |
cfg |
Server configuration |
Returns Server
config()¶
Returns a new Config builder for fluent configuration.
Returns Config
Example
import "httpserver"
cfg = httpserver::config()
.host("0.0.0.0")
.port(9000)
.workers(20)
.logging("combined")
.cors()
server = httpserver::create_with_config(cfg)
Config¶
A fluent configuration builder. All setter methods return self for chaining.
Config.new()¶
Creates a Config with defaults: host="localhost", port=8080, workers=10, timeout_ms=5000, log="combined", CORS disabled.
Setters¶
| Method | Parameter Type | Description | Default |
|---|---|---|---|
.host(v) |
string |
Hostname or IP address to bind | "localhost" |
.port(v) |
integer |
Port number | 8080 |
.workers(v) |
integer |
Maximum concurrent request worker tasks | 10 |
.timeout(v) |
integer |
Accept-poll timeout in milliseconds | 5000 |
.logging(v) |
string\|boolean |
"combined", "simple", "none", or false to disable |
"combined" |
.cors(origin?) |
string |
Enables CORS; sets Access-Control-Allow-Origin |
"*" |
Server¶
The main server object. Returned by httpserver::create() or httpserver::create_with_config().
Route Registration¶
All route methods return self for chaining.
| Method | Description |
|---|---|
.get(path, handler) |
Register a GET route |
.post(path, handler) |
Register a POST route |
.put(path, handler) |
Register a PUT route |
.patch(path, handler) |
Register a PATCH route |
.delete(path, handler) |
Register a DELETE route |
.all_methods(path, handler) |
Register a route matching any HTTP method |
handler is a lambda with signature with (req, res) do ... end.
Route patterns support:
- Literal segments: /users/profile
- Named parameters: /users/:id — accessible via req.param("id")
- Wildcard suffix: /static/* — captures remaining path in req.param("*")
.use(mw)¶
Adds a middleware function to the request pipeline. Middlewares run in registration order before the matched route handler.
Parameters
| Type | Name | Description |
|---|---|---|
lambda |
mw |
Middleware with signature with (req, res, nxt) do ... nxt() end |
Returns Server
Call
nxt()to pass control to the next middleware. Omit it to stop the chain (short-circuit).
.static_files(url_prefix, dir)¶
Registers middleware to serve static files from a filesystem directory.
Parameters
| Type | Name | Description |
|---|---|---|
string |
url_prefix |
URL path prefix (e.g. "/public") |
string |
dir |
Filesystem directory to serve from (e.g. "./public") |
Returns Server
MIME types are inferred from file extensions. Responds with 404 for missing files.
.group(prefix)¶
Creates a Router that prepends prefix to all registered routes.
Parameters
| Type | Name | Description |
|---|---|---|
string |
prefix |
URL path prefix (e.g. "/api") |
Returns Router
Lifecycle Hooks¶
| Method | Handler Signature | Description |
|---|---|---|
.on_start(hook) |
with () do ... end |
Called after the server starts listening |
.on_stop(hook) |
with () do ... end |
Called before the server stops |
.on_error(hook) |
with (err, req, res) do |
Called when a route handler throws an error |
All hook methods return self.
.start(block?)¶
Starts the server.
Parameters
| Type | Name | Description | Default |
|---|---|---|---|
boolean |
block |
If true, blocks the calling thread until stop() is called |
false |
Returns Server
When block=false (default), the accept loop runs in a background task.
.stop()¶
Stops the server, calling the on_stop hook if registered.
Returns Server
Router¶
A route group that delegates to a Server with a common path prefix. Returned by server.group(prefix).
Methods¶
| Method | Description |
|---|---|
.get(path, handler) |
Registers GET prefix+path |
.post(path, handler) |
Registers POST prefix+path |
.put(path, handler) |
Registers PUT prefix+path |
.patch(path, handler) |
Registers PATCH prefix+path |
.delete(path, handler) |
Registers DELETE prefix+path |
.all_methods(path, handler) |
Registers for all methods under prefix+path |
.group(sub_prefix) |
Creates a nested Router with combined prefix |
Request¶
Passed as the first argument to every route handler and middleware.
Methods¶
| Method | Returns | Description |
|---|---|---|
.method() |
string |
HTTP method in uppercase ("GET", "POST", …) |
.path() |
string |
URL path (e.g. "/users/42") |
.query() |
hashmap |
All query-string parameters |
.query_param(name, default?) |
string\|any |
Single query param; returns default if absent (null) |
.headers() |
hashmap |
All request headers (keys are lowercased) |
.header(name) |
string\|null |
Single header by name (case-insensitive) |
.body() |
string |
Raw request body |
.remote_addr() |
string |
Client IP address |
.id() |
integer |
Internal request ID |
.param(name) |
string\|null |
Route parameter by name (e.g. "id" for /:id) |
.params() |
hashmap |
All route parameters |
.content_type() |
string\|null |
Value of the Content-Type header |
.is_json() |
boolean |
true if Content-Type starts with application/json |
.is_form() |
boolean |
true if Content-Type is application/x-www-form-urlencoded |
.json() |
any |
Parses and returns the body as JSON (cached) |
.form() |
hashmap |
Parses URL-encoded form body (cached) |
.cookie(name) |
string\|null |
Returns a cookie value by name |
.cookies() |
hashmap |
All cookies parsed from the Cookie header (cached) |
Response¶
Passed as the second argument to every route handler and middleware.
Sending Responses¶
| Method | Description |
|---|---|
.send(status, body) |
Send with explicit status code; body is a string or bytes |
.text(body, status?) |
Send text/plain (default 200) |
.html(body, status?) |
Send text/html |
.json(body, status?) |
Send application/json; auto-serializes non-string values |
.file(fs_path, status?) |
Send file contents; MIME inferred from extension |
.redirect(location, status?) |
Send redirect (default 302) |
Convenience Error Responses¶
| Method | Status | Default body |
|---|---|---|
.not_found(msg?) |
404 | "Not Found" |
.bad_request(msg?) |
400 | "Bad Request" |
.unauthorized(msg?) |
401 | "Unauthorized" |
.forbidden(msg?) |
403 | "Forbidden" |
.server_error(msg?) |
500 | "Internal Server Error" |
Headers and Cookies¶
| Method | Description |
|---|---|
.header(key, value) |
Set a response header; returns self |
.cookie(name, value, path?, max_age?, http_only?, secure?, same_site?) |
Append a Set-Cookie header; returns self |
.clear_cookie(name, path?) |
Expire a cookie immediately; returns self |
.no_cache() |
Set Cache-Control: no-store headers; returns self |
cookie() defaults: path="/", max_age=null (session), http_only=true, secure=false, same_site="Lax".
State¶
| Method | Returns | Description |
|---|---|---|
.sent() |
boolean |
true if a response has already been sent |
.status_code() |
integer |
The status code that was (or will be) sent |
Examples¶
Basic server¶
import "httpserver"
server = httpserver::create(8080)
server.get("/", with (req, res) do
res.html("<h1>Hello from Kiwi</h1>")
end)
server.get("/api/greet/:name", with (req, res) do
name = req.param("name")
res.json({ "greeting": "Hello, " + name + "!" })
end)
server.post("/api/echo", with (req, res) do
res.text("You sent: " + req.body())
end)
server.start()
Middleware¶
import "httpserver"
import "time"
server = httpserver::create(8080)
# Timing middleware
server.use(with (req, res, nxt) do
t0 = time::ticks()
nxt()
elapsed = (time::ticks() - t0) * 1000
eprintln "handled in ${elapsed}ms"
end)
# Auth guard
server.use(with (req, res, nxt) do
token = req.header("authorization")
if token == null || token != "Bearer secret"
res.unauthorized("Invalid token")
return # Do NOT call nxt() — short-circuit the chain
end
nxt()
end)
server.get("/secure", with (req, res) do
res.text("You're in!")
end)
server.start()
Route groups¶
import "httpserver"
server = httpserver::create(8080)
api = server.group("/api/v1")
api.get("/users", with (req, res) do
res.json([{ "id": 1, "name": "Alice" }])
end)
api.get("/users/:id", with (req, res) do
res.json({ "id": req.param("id") })
end)
api.post("/users", with (req, res) do
data = req.json()
res.json({ "created": data }, 201)
end)
server.start()
Static file serving¶
import "httpserver"
server = httpserver::create(8080)
# Serve ./public/ at /public/*
server.static_files("/public", "./public")
server.get("/", with (req, res) do
res.file("./public/index.html")
end)
server.start()
CORS and configuration¶
import "httpserver"
cfg = httpserver::config()
.host("0.0.0.0")
.port(9000)
.cors("https://myapp.example.com")
.logging("simple")
server = httpserver::create_with_config(cfg)
server.get("/api/data", with (req, res) do
res.json({ "items": [1, 2, 3] })
end)
server.start()
Cookies¶
import "httpserver"
server = httpserver::create(8080)
server.post("/login", with (req, res) do
body = req.json()
if body["username"] == "admin" && body["password"] == "secret"
res.cookie("session", "tok_abc123", "/", 3600)
res.json({ "ok": true })
else
res.unauthorized("Bad credentials")
end
end)
server.post("/logout", with (req, res) do
res.clear_cookie("session")
res.json({ "ok": true })
end)
server.get("/profile", with (req, res) do
sid = req.cookie("session")
if sid == null
res.unauthorized()
return
end
res.json({ "user": "admin" })
end)
server.start()
Lifecycle hooks and error handling¶
import "httpserver"
server = httpserver::create(8080)
server.on_start(with () do
eprintln "Server is up!"
end)
server.on_stop(with () do
eprintln "Server is going down..."
end)
server.on_error(with (err, req, res) do
eprintln "Error on ${req.path()}: ${err}"
res.json({ "error": err.to_string() }, 500)
end)
server.get("/risky", with (req, res) do
error::raise("something went wrong")
end)
server.start()
Blocking mode (serve from main thread)¶
import "httpserver"
import "task"
server = httpserver::create(8080)
server.get("/", with (req, res) do
res.text("Hello!")
end)
# Spawn a background task that stops the server after 10 seconds
task::spawn(with () do
task::sleep(10000)
server.stop()
end, [])
# Block the main thread — returns when stop() is called
server.start(true)
println "Server stopped."