Asynchronous I/O & HTTP

Nostos provides fully asynchronous I/O for network and file access. Write synchronous-looking code while the runtime handles concurrency transparently.

HTTP Client

Make HTTP requests with the built-in Http module. Operations are non-blocking.

main() = {
    try {
        resp = Http.get("https://httpbin.org/ip")
        println("Your IP: " ++ resp.body)
    } catch { e ->
        println("Error: " ++ e)
    }
}

HTTP Server

Build web servers with stdlib.server. The serve() function spawns a lightweight process per request.

use stdlib.server.{serve, respondText, respond404}

handle(req) = match req.path {
    "/" -> respondText(req, "Hello from Nostos!")
    "/echo" -> respondText(req, req.body)
    _ -> respond404(req)
}

main() = {
    println("Server: http://localhost:8080")
    serve(8080, handle)
}

Query Parameters

Use getParam() to look up values in query params, cookies, form params, or headers.

use stdlib.server.{serve, getParam, respondText, respond400}

handleSearch(req) = {
    query = getParam(req.queryParams, "q")
    limit = getParam(req.queryParams, "limit")

    if query == "" then respond400(req, "Missing 'q' parameter")
    else respondText(req, "Searching: " ++ query)
}

main() = serve(8080, handleSearch)

Form Handling

Process POST data with req.formParams. URL decoding is automatic.

use stdlib.server.{serve, getParam, respondText, respond400, respond405}

handleSubmit(req) = {
    if req.method != "POST" then respond405(req)
    else {
        name = getParam(req.formParams, "name")
        email = getParam(req.formParams, "email")

        if name == "" then respond400(req, "Name required")
        else respondText(req, "Thanks, " ++ name ++ "!")
    }
}

Type-Safe Request Parsing

Use requestToType to parse query and form parameters directly into typed records. Returns Ok(record) on success or Err(message) with details about missing or invalid fields.

use stdlib.server.{serve, respondText, respond400}

type Result[T, E] = Ok(T) | Err(E)
type Option[T] = Some(T) | None

# Define your parameter type
type CreateUserParams = { name: String, age: Int, email: Option[String] }

handleCreateUser(req) = {
    # Sugar syntax: compiler infers "CreateUserParams" from type annotation
    result: Result[CreateUserParams, String] = req.toType()

    match result {
        Ok(params) -> {
            # params.name is String, params.age is Int
            # params.email is Option[String] (None if not provided)
            respondText(req, "Created user: " ++ params.name ++ ", age " ++ show(params.age))
        }
        Err(msg) -> respond400(req, msg)
    }
}

main() = serve(8080, handleCreateUser)

Supported field types:

  • String - Used as-is from request
  • Int, Int32, Int64 - Parsed from string
  • Float, Float64 - Parsed from string
  • Bool - Accepts "true"/"false", "1"/"0", "yes"/"no"
  • Option[T] - Defaults to None if field is missing

Error messages include the field name and what went wrong:

# Missing required field:
# Err("missing required field 'age'")

# Invalid type:
# Err("field 'age': expected Int, got 'not_a_number'")

Cookies

Read cookies with getParam(req.cookies, name). Set cookies with helper functions.

use stdlib.server.{serve, getParam, respondText, respondTextWith, setCookieWithAge, clearCookie}

handleLogin(req) = {
    user = getParam(req.queryParams, "user")
    respondTextWith(req, "Welcome " ++ user, [setCookieWithAge("session", user, 3600)])
}

handleLogout(req) = respondTextWith(req, "Logged out", [clearCookie("session")])

handleProfile(req) = {
    session = getParam(req.cookies, "session")
    if session == "" then respondText(req, "Not logged in")
    else respondText(req, "Session: " ++ session)
}

HTML Templating

Build type-safe HTML with Html(...). Tags are scoped functions inside the constructor.

use stdlib.html.{Html, render}
use stdlib.server.{serve, respondHtml}

userCard(name: String, email: String) = Html(
    el("div", [("class", "card")], [
        h3(name),
        p("Email: " ++ email)
    ])
)

handleHome(req) = {
    content = Html(div([
        h1("Welcome"),
        userCard("Alice", "alice@example.com"),
        userCard("Bob", "bob@example.com")
    ]))
    respondHtml(req, render(content))
}

main() = serve(8080, handleHome)

URL Routing

Use Server.matchPath to extract path parameters like :id.

use stdlib.server.{serve, respondText, respond404}

route(req) = {
    path = req.path

    if path == "/" then respondText(req, "Home")
    else {
        params = Server.matchPath(path, "/users/:id")
        if length(params) == 1 then {
            (_, userId) = head(params)
            respondText(req, "User: " ++ userId)
        } else respond404(req)
    }
}

main() = serve(8080, route)

WebSockets

Handle WebSocket connections with req.isWebSocket and the WebSocket module.

use stdlib.server.{serve, respondText}

echoLoop(ws) = {
    ok = try {
        msg = WebSocket.recv(ws)
        WebSocket.send(ws, "Echo: " ++ msg)
        true
    } catch { _ -> false }
    if ok then echoLoop(ws) else WebSocket.close(ws)
}

route(req) = {
    if req.isWebSocket then {
        ws = WebSocket.accept(req.id)
        echoLoop(ws)
    } else respondText(req, "WebSocket server at ws://localhost:8080/")
}

main() = serve(8080, route)

PostgreSQL with Connection Pool

Use an mvar to create a simple connection pool. Get a connection for each request, return it when done.

use stdlib.server.{serve, getParam, respondText, respond400}

# Connection pool using mvar
mvar pool: List[Int] = []

getConn() = match pool {
    [] -> Pg.connect("host=localhost user=postgres password=postgres")
    [conn | rest] -> { pool = rest; conn }
}

releaseConn(conn) = { pool = conn :: pool }

# Handlers
listUsers(req) = {
    conn = getConn()
    rows = Pg.query(conn, "SELECT id, name FROM users", [])
    releaseConn(conn)
    respondText(req, show(rows))
}

createUser(req) = {
    name = getParam(req.formParams, "name")
    if name == "" then respond400(req, "name required")
    else {
        conn = getConn()
        Pg.execute(conn, "INSERT INTO users (name) VALUES ($1)", [name])
        releaseConn(conn)
        respondText(req, "Created: " ++ name)
    }
}

route(req) = match req.path {
    "/users" -> if req.method == "POST" then createUser(req) else listUsers(req)
    _ -> respondText(req, "GET/POST /users")
}

main() = serve(8080, route)

stdlib.server API:

  • serve(port, handler) - Start server with spawn-per-request
  • getParam(params, key) - Look up key in param list
  • req.toType() - Parse request params to typed record (with type annotation)
  • respondText(req, body) - Send text response
  • respondHtml(req, body) - Send HTML response
  • respondJson(req, body) - Send JSON response
  • respond400(req, msg) - Bad Request
  • respond404(req) - Not Found
  • redirect(req, url) - 302 redirect
  • setCookieWithAge(name, value, seconds) - Cookie header tuple
  • wsLoop(ws, handler) - WebSocket message loop