Example: HTTP Server

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:

  • Method is a plain enum with behavior added via extend
  • Header is a struct plain data, no constructor needed
  • Request is a class with private state (query_params) and public accessors
  • parse_request uses panic for error signaling callers must handle ParseError
  • get_header returns string? 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:

  • StatusCode is an enum with an explicit u16 underlying type
  • Response uses method chaining (each builder method returns Self)
  • Static factory methods (Response::ok(), Response::not_found()) for common responses
  • serialize is a const method 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:

  • Handler is a type alias for a function pointer that takes a request and may panic
  • Route is a struct plain data with aggregate init
  • Router uses method chaining for registration
  • resolve is const routing 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++" import brings in socket operations no unsafe block needed because tcp_listen, tcp_accept, etc. take and return value types (i32, string), not raw pointers
  • finally blocks ensure sockets are closed on all exit paths (normal, panic, early return)
  • try/catch is used in expression form to produce a Response on both success and failure
  • Nested try/catch the outer one handles parse errors, the inner one handles handler panics
  • handle_client is priv internal 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
  • main handles all panics from srv.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

FeatureWhere
ClassesRequest, Response, Server, Router
StructsHeader, Route
EnumsMethod, StatusCode
ExtendsMethod::from_string, StatusCode::reason
InterfacesHandler type alias for function pointers
PanicError handling throughout
Try/CatchRequest parsing, handler execution
FinallySocket cleanup
ClosuresFallback handler
Nullable typesget_header returns string?
Null coalescingcontent_length uses ??
Type aliasesHandler type
C++ interopSocket API via ffi "c++"
MatchMethod parsing, status code reasons
ModulesFile-per-module structure
Const methodsserialize, resolve, get_header
Method chainingResponse builder, router registration