Web Servers (planned)

Luma includes a lightweight, expressive and predictable approach for building web servers.
The design focuses on three principles:

  • Minimal boilerplate
  • Handlers are just functions
  • Clear and explicit types

This document describes the recommended way to build HTTP servers and APIs in Luma using the standard server module.

Getting Started

The simplest possible web server listens on a port and responds with “Hello World”.

fn hello(req: Request) -> Response {
    return text("Hello World")
}

fn main() {
    routes: [Route] = [
        get("/", hello),
    ]

    config: ServerConfig = ServerConfig {
        port: 3000,
        routes: routes,
        middleware: [],
        on_error: nil,
    }

    server.start(config)
}

This example demonstrates the fundamental concepts:

  • A handler is just a function: fn(req: Request) -> Response
  • Routes are collected in a list: [Route]
  • The server is started by passing a ServerConfig struct

Routes

Routes are defined using helper functions that associate an HTTP method, a path, and a handler.

routes: [Route] = [
    get("/", home),
    get("/users", list_users),
    post("/users", create_user),
]

A route is internally represented as:

struct Route {
    method: str
    path: str
    handler: Handler   // alias for (Request) -> Response
}

Defining Handlers

Handlers always follow the same form:

fn handler_name(req: Request) -> Response {
    // ...
}

Example:

fn list_users(req: Request) -> Response {
    users: [User] = db.query_users()
    return json({ users: users })
}

Request Object

Every handler receives a Request value containing:

struct Request {
    method: str
    path: str
    url: str

    headers: {str: str}
    query: {str: str}
    params: {str: str}

    body_bytes: [byte]
}

Example usage inside a handler:

fn example(req: Request) -> Response {
    token: ?str = req.headers.get("Authorization")
    search: ?str = req.query.get("search")
    id: ?str = req.params.get("id")     // for dynamic routes

    return text("OK")
}

You may extend Request by adding convenience methods inside an impl block:

impl Request {
    fn json(self) -> {str: any} {
        // parse self.body_bytes
    }
}

Responses

All HTTP responses are instances of:

struct Response {
    status: int
    headers: {str: str}
    body: [byte]
}

A few helper functions are provided:

fn text(body: str) -> Response
fn json(body: {str: any}) -> Response
fn redirect(location: str) -> Response
fn file_download(path: str) -> Response

Examples:

return text("Hello")

return json({ message: "Created" })

return redirect("/login")

To return custom status codes:

fn not_found() -> Response {
    return Response {
        status: 404,
        headers: {},
        body: "Not found".to_bytes(),
    }
}

Middleware

Middleware functions wrap handlers and allow pre- and post-processing of requests.

A middleware has the form:

type Handler = (Request) -> Response
type Middleware = (Request, Handler) -> Response

Example: logging middleware

fn log_middleware(req: Request, next: Handler) -> Response {
    start: int = now().unix_ms()
    res: Response = next(req)
    duration: int = now().unix_ms() - start

    print("${req.method} ${req.path} - ${duration}ms")
    return res
}

Example: authentication middleware

fn auth_middleware(req: Request, next: Handler) -> Response {
    token: ?str = req.headers.get("Authorization")
    if token == nil || !valid_token(*token) {
        return json({ error: "Unauthorized" })
    }
    return next(req)
}

Middleware is applied through the ServerConfig:

config: ServerConfig = ServerConfig {
    port: 3000,
    routes: routes,
    middleware: [log_middleware, auth_middleware],
    on_error: nil,
}

Error Handling

Luma allows defining a custom error handler that receives:

  • The error value
  • The original request
fn my_error_handler(err: Error, req: Request) -> Response {
    print("Error: ${err.message}")
    return json({ error: "Internal Server Error" })
}

Add it to the configuration:

config.on_error = my_error_handler

Dynamic Routes

Paths may include parameters:

routes: [Route] = [
    get("/users/:id", get_user),
]

Inside the handler:

fn get_user(req: Request) -> Response {
    id: ?str = req.params.get("id")
    if id == nil {
        return not_found()
    }
    user: ?User = db.find_user(*id)
    if user == nil {
        return not_found()
    }
    return json({ user: user })
}

Complete Example

fn get_users(req: Request) -> Response {
    users: [User] = db.query_users()
    return json({ users: users })
}

fn create_user(req: Request) -> Response {
    data: {str: any} = req.json()
    new_id: int = db.insert_user(data)
    return json({ id: new_id })
}

fn health(req: Request) -> Response {
    return json({ status: "ok", timestamp: now().unix() })
}

routes: [Route] = [
    get("/api/users", get_users),
    post("/api/users", create_user),
    get("/health", health),
]

conf: ServerConfig = ServerConfig {
    port: 3000,
    routes: routes,
    middleware: [],
    on_error: nil,
}

server.start(conf)

Summary

Luma’s web server design centers on:

  • Clear types (Request, Response, Route, ServerConfig)
  • Simple handlers (fn(req) -> Response)
  • Explicit configuration
  • Familiar, composable middleware
  • Built-in helpers for JSON, text, redirects, and files

This approach provides a minimal but powerful foundation for building REST APIs, websites, and backend services - while staying fully aligned with Luma’s core principles: clarity, safety, and simplicity.

Last updated on