Example: HTTP Server
This example builds a minimal HTTP server from scratch, demonstrating how Kairo’s features compose in a real application. The server parses requests, routes them to handlers, and sends responses — all with type-safe error handling and automatic memory management via AMT.
Project Structure
http-server/
http-server.k <- library entry
request.k
response.k
router.k
server.k
main.k
Request Parsing
request.k defines the HTTP request model and a parser that converts raw bytes into a typed
request object.
// request.k
import std
pub enum Method {
Get,
Post,
Put,
Delete,
Patch,
Head,
Options,
}
extend Method {
static fn from_string(s: string) panic -> Method {
match s {
case "GET" { return .Get }
case "POST" { return .Post }
case "PUT" { return .Put }
case "DELETE" { return .Delete }
case "PATCH" { return .Patch }
case "HEAD" { return .Head }
case "OPTIONS" { return .Options }
default {
panic ParseError("unknown HTTP method", 0)
}
}
}
}
pub struct Header {
var name: string
var value: string
}
pub class Request {
pub var method: Method
pub var path: string
pub var headers: [Header]
pub var body: string
priv var query_params: {string: string}
fn Request(self, method: Method, path: string) {
self.method = method
self.path = path
self.headers = []
self.body = ""
self.query_params = {}
}
pub fn get_header(const self, name: string) -> string? {
for h in self.headers {
if h.name == name {
return h.value
}
}
return null
}
pub fn content_length(const self) -> i32 {
var cl? = self.get_header("Content-Length")
return cl as i32 ?? 0
}
}
pub class ParseError {
pub var message: string
pub var position: i32
fn ParseError(self, msg: string, pos: i32) {
self.message = msg
self.position = pos
}
}
pub fn parse_request(raw: string) panic -> Request {
var lines = raw.split("\r\n")
if lines.len() == 0 {
panic ParseError("empty request", 0)
}
// Parse request line: "GET /path HTTP/1.1"
var parts = lines[0].split(" ")
if parts.len() < 3 {
panic ParseError("malformed request line", 0)
}
var method = Method::from_string(parts[0])
var path = parts[1]
var req = Request(method, path)
// Parse headers
var i = 1
while i < lines.len() && lines[i].len() > 0 {
var colon = lines[i].index_of(":")
if colon < 0 {
panic ParseError("malformed header", i)
}
req.headers.push(Header {
name: lines[i].substring(0, colon).trim(),
value: lines[i].substring(colon + 1).trim(),
})
i += 1
}
// Remaining lines are the body
if i + 1 < lines.len() {
req.body = lines[i + 1]
}
return req
}
Key patterns:
Methodis a plain enum with behavior added viaextendHeaderis a struct plain data, no constructor neededRequestis a class with private state (query_params) and public accessorsparse_requestusespanicfor error signaling callers must handleParseErrorget_headerreturnsstring?for safe nullable access
Response Building
response.k defines the response model with a builder-style API.
// response.k
import std
pub enum StatusCode derives u16 {
Ok = 200,
Created = 201,
NoContent = 204,
BadRequest = 400,
NotFound = 404,
MethodNotAllowed = 405,
InternalError = 500,
}
extend StatusCode {
fn reason(self) const -> string {
match self {
case .Ok { return "OK" }
case .Created { return "Created" }
case .NoContent { return "No Content" }
case .BadRequest { return "Bad Request" }
case .NotFound { return "Not Found" }
case .MethodNotAllowed { return "Method Not Allowed" }
case .InternalError { return "Internal Server Error" }
}
}
}
pub class Response {
pub var status: StatusCode
priv var headers: {string: string}
priv var body: string
fn Response(self, status: StatusCode) {
self.status = status
self.headers = {}
self.body = ""
}
pub fn header(self, name: string, value: string) -> Self {
self.headers[name] = value
return self
}
pub fn content_type(self, mime: string) -> Self {
return self.header("Content-Type", mime)
}
pub fn text(self, content: string) -> Self {
self.body = content
return self.content_type("text/plain")
}
pub fn json(self, content: string) -> Self {
self.body = content
return self.content_type("application/json")
}
pub fn serialize(const self) -> string {
var line = f"HTTP/1.1 {self.status as u16} {self.status.reason()}\r\n"
for (name, value) in self.headers {
line = line + f"{name}: {value}\r\n"
}
line = line + f"Content-Length: {self.body.len()}\r\n"
line = line + "\r\n"
line = line + self.body
return line
}
pub static fn ok() -> Response {
return Response(StatusCode::Ok)
}
pub static fn not_found() -> Response {
return Response(StatusCode::NotFound)
.text("404 Not Found")
}
pub static fn bad_request(msg: string) -> Response {
return Response(StatusCode::BadRequest)
.text(msg)
}
pub static fn internal_error() -> Response {
return Response(StatusCode::InternalError)
.text("Internal Server Error")
}
}
Key patterns:
StatusCodeis an enum with an explicitu16underlying typeResponseuses method chaining (each builder method returnsSelf)- Static factory methods (
Response::ok(),Response::not_found()) for common responses serializeis aconstmethod it does not modify the response
Router
router.k maps method + path combinations to handler functions.
// router.k
import std
import request::{Request, Method}
import response::Response
pub type Handler = fn(*Request) panic -> Response
struct Route {
var method: Method
var path: string
var handler: Handler
}
pub class Router {
priv var routes: [Route]
priv var not_found_handler: Handler
fn Router(self) {
self.routes = []
self.not_found_handler = fn (req: *Request) panic -> Response {
return Response::not_found()
}
}
pub fn get(self, path: string, handler: Handler) -> Self {
self.routes.push(Route { method: .Get, path: path, handler: handler })
return self
}
pub fn post(self, path: string, handler: Handler) -> Self {
self.routes.push(Route { method: .Post, path: path, handler: handler })
return self
}
pub fn put(self, path: string, handler: Handler) -> Self {
self.routes.push(Route { method: .Put, path: path, handler: handler })
return self
}
pub fn delete(self, path: string, handler: Handler) -> Self {
self.routes.push(Route { method: .Delete, path: path, handler: handler })
return self
}
pub fn fallback(self, handler: Handler) -> Self {
self.not_found_handler = handler
return self
}
pub fn resolve(const self, req: *Request) -> Handler {
for route in self.routes {
if route.method == req->method && route.path == req->path {
return route.handler
}
}
return self.not_found_handler
}
}
Key patterns:
Handleris a type alias for a function pointer that takes a request and may panicRouteis a struct plain data with aggregate initRouteruses method chaining for registrationresolveisconstrouting does not mutate the router
Server
server.k ties everything together with a TCP accept loop. It uses C++ interop for socket
operations.
// server.k
import std
import request::{parse_request, Request, ParseError}
import response::Response
import router::Router
ffi "c++" import "socket_api.hh" as net
pub class Server {
priv var router: Router
priv var port: i32
priv var running: bool
fn Server(self, port: i32) {
self.router = Router()
self.port = port
self.running = false
}
pub fn routes(self) -> *Router {
return &self.router
}
pub fn start(self) panic {
var listener = net::tcp_listen(self.port)
if listener < 0 {
panic std::Error::IO(f"failed to bind to port {self.port}")
}
finally {
net::tcp_close(listener)
}
std::println(f"listening on :{self.port}")
self.running = true
while self.running {
var client = net::tcp_accept(listener)
if client < 0 {
continue
}
self.handle_client(client)
}
}
pub fn stop(self) {
self.running = false
}
priv fn handle_client(self, fd: i32) {
finally {
net::tcp_close(fd)
}
var raw = net::tcp_read(fd, 8192)
var response = try {
var req = parse_request(raw)
var handler = self.router.resolve(&req)
try {
handler(&req)
} catch {
Response::internal_error()
}
} catch e: ParseError {
Response::bad_request(e.message)
} catch {
Response::internal_error()
}
net::tcp_write(fd, response.serialize())
}
}
Key patterns:
ffi "c++" importbrings in socket operations no unsafe block needed becausetcp_listen,tcp_accept, etc. take and return value types (i32,string), not raw pointersfinallyblocks ensure sockets are closed on all exit paths (normal, panic, early return)try/catchis used in expression form to produce aResponseon both success and failure- Nested
try/catchthe outer one handles parse errors, the inner one handles handler panics handle_clientisprivinternal implementation detail
Putting It Together
main.k wires up routes and starts the server.
// main.k
import std
import server::Server
import request::{Request, Method}
import response::Response
fn handle_index(req: *Request) panic -> Response {
return Response::ok()
.json("{\"status\": \"running\"}")
}
fn handle_health(req: *Request) panic -> Response {
return Response::ok()
.text("ok")
}
fn handle_echo(req: *Request) panic -> Response {
if req->method != .Post {
return Response(StatusCode::MethodNotAllowed)
.text("POST only")
}
return Response::ok()
.content_type("text/plain")
.text(req->body)
}
fn main() {
var srv = Server(8080)
srv.routes()
->get("/", handle_index)
->get("/health", handle_health)
->post("/echo", handle_echo)
->fallback(fn (req: *Request) panic -> Response {
return Response::not_found()
})
try {
srv.start()
} catch e: std::Error::IO {
std::println(f"server error: {e}")
} catch {
std::println("unexpected error")
}
}
Key patterns:
- Handlers are plain functions passed as function pointers
- The fallback handler is an inline closure
mainhandles all panics fromsrv.start()no unhandled errors escape
Building and Running
kairo main.k -o server
./server
listening on :8080
curl http://localhost:8080/
# {"status": "running"}
curl http://localhost:8080/health
# ok
curl -X POST -d "hello" http://localhost:8080/echo
# hello
curl http://localhost:8080/nonexistent
# 404 Not Found
Features Used
| Feature | Where |
|---|---|
| Classes | Request, Response, Server, Router |
| Structs | Header, Route |
| Enums | Method, StatusCode |
| Extends | Method::from_string, StatusCode::reason |
| Interfaces | Handler type alias for function pointers |
| Panic | Error handling throughout |
| Try/Catch | Request parsing, handler execution |
| Finally | Socket cleanup |
| Closures | Fallback handler |
| Nullable types | get_header returns string? |
| Null coalescing | content_length uses ?? |
| Type aliases | Handler type |
| C++ interop | Socket API via ffi "c++" |
| Match | Method parsing, status code reasons |
| Modules | File-per-module structure |
| Const methods | serialize, resolve, get_header |
| Method chaining | Response builder, router registration |