/ Projects

Tapak

OCaml

Tapak is a type-safe, contract-first web framework for OCaml 5, built on Eio. It pairs direct-style concurrency with type-checked routing and generates OpenAPI documents from your route definitions. It provides:

  • Type-level routing: declare the inputs your handler takes, and Tapak extracts and parses them from the path, query parameters, cookies, headers, and body. The types stay checked end to end.
  • Request validation: define a schema once and get parsing, validation, and error responses from it.
  • Realtime communication: built-in WebSocket and Server-Sent-Events support.
  • OpenAPI documentation: generated from your route definitions, so the docs match the routes they describe.

Installation

Tapak is not on Opam yet (soon!). Until then, install it by pinning the Git repository or by using Nix.

Install with Opam

opam pin add cows https://github.com/syaiful6/cows.git
opam pin add tapak https://github.com/syaiful6/tapak.git

# Install dependencies and build
opam install tapak --deps-only

Install with Nix

Add Tapak as flake input in your flake.nix:

{
  inputs = {
    tapak.url = "github:syaiful6/tapak";
  };
  outputs = { self, tapak, ... }: {
    # Use tapak in your development or package build
    # you know how to do this, you use nix btw.
  }
}

Quick Start

This example wires up type-safe routing, a logging middleware, and Eio:

let home_handler () =
  Tapak.html ~status:`OK "<h1>Welcome to Tapak!</h1>"

let user_handler user_id =
  (* user_id is automatically parsed as int64 *)
  let html = Printf.sprintf "<h1>User %Ld</h1>" user_id in
  Tapak.html ~status:`OK html

let api_handler id name =
  (* Both id (int64) and name (string) are type-safe *)
  let json = Printf.sprintf {|{"user_id": "%Ld","name": "%s"}|} id name in
  Tapak.json ~status:`OK json

let app env =
  let now () = Eio.Time.now (Eio.Stdenv.clock env) in
  Tapak.(
    Router.(
      of_list
        [ get (s "") |> unit |> into home_handler
        ; get (s "users" / int64) |> into user_handler
        ; get (s "api" / s "users" / int64 / str) |> into api_handler
        ])
    |> use
         (module Middleware.Request_logger)
         (Middleware.Request_logger.args ~now ~trusted_proxies:[] ()))

let () =
  Eio_main.run @@ fun env ->
  Eio.Switch.run @@ fun sw ->
  let port = 3000 in
  let address = `Tcp (Eio.Net.Ipaddr.V4.any, port) in
  let socket =
    Eio.Net.listen ~reuse_addr:true ~backlog:1024 ~sw env#net address
  in
  Tapak.run
    ~on_error:(fun exn ->
      Log.warn (fun f -> f "Uncaught exception %s" (Printexc.to_string exn)))
    socket
    (app env)