Xo is experimental. The source code is not public yet; it will be soon. Read the release notes
Menu · A small web service

Docs

A small web service

webservice/main.xo

use std/http

pub struct Todo {
  pub id: Int
  pub title: Str
  pub done: Bool
} derive Json

pub enum ApiErr {
  NotFound(id: Int)
}

fn (e: ApiErr) status() -> Int {
  match e {
    NotFound(_) => 404
  }
}

fn (e: ApiErr) display() -> Str {
  match e {
    NotFound(id) => "todo ${id} not found"
  }
}

fn find(todos: List[Todo], id: Int) -> Todo ! ApiErr {
  todos.find(fn(t) { t.id == id }).or_err(ApiErr.NotFound(id: id))
}

fn routes(todos: List[Todo]) -> http.Router {
  var r = http.router()
  r.handle("GET /todos", fn(_req) { http.json(200, todos) })
  r.handle("GET /todos/{id:Int}", fn(req) -> http.Response ! ApiErr {
    http.json(200, find(todos, req.param_int("id"))?)
  })
  r
}

fn main(os: Os) ! {
  let todos = [Todo{id: 1, title: "write the tour", done: false}]
  http.server(os.net.listen(":8080")?, routes(todos)).serve()?
}

test "missing todo is a 404" {
  let r = routes([])
  let resp = r.call(http.new_request("GET", "/todos/7"))
  expect(resp.status) == 404
  expect(resp.text() ?? "") == "{\"error\":\"todo 7 not found\"}"
}

Output:

(serves on :8080; try GET /todos and GET /todos/1)
  • "GET /todos/{id:Int}" declares a typed path parameter. A request with a non numeric id gets a 400 before the handler runs.
  • derive Json on Todo generates the encoder http.json uses.
  • A handler can fail. ApiErr has status and display methods, so a NotFound becomes a 404 with a JSON error body. Any other error is a 500, and a fault in a handler is isolated to that request.
  • r.call(request) runs the router in process, so the test needs no network.
  • main is the only place that touches the network: os.net.listen gives the listener to the server.