Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Vif, a simple webserver in OCaml

Vif is a small program that allows you to initiate a web server (http/1.1 & h2) from an OCaml script:

$ opam install vif
$ cat >main.ml <<EOF
#require "vif" ;;
open Vif ;;

let default req _server () =
  let str = "Hello World!\n" in
  let* () = Response.with_string req str in
  Response.respond `OK
;;

let routes =
  let open Vif.Uri in
  let open Vif.Route in
  [ get (rel /?? nil) --> default ]
;;

let () = Miou_unix.run @@ fun () ->
  Vif.run routes ()
;;
EOF
$ vif main.ml --pid vif.pid &
[1] 1337
$ curl http://localhost:8080/
Hello World!
$ kill -SIGINT $(cat vif.pid)

The aim is to have a small web server for cheap and to be able to iterate on it quickly without necessarily having a workflow that involves compiling and executing an OCaml project. The OCaml script is executed by vif just as it could be executed by ocaml and it describes what your webserver should do.

In this short tutorial, we will see how to use Vif to implement more or less complex web services.

Vif as a library

Of course, vif is also a library. You don't need to write an OCaml script to get a web server. You can also develop your own application and compile it with Vif to get an executable that will be your web server.

However, in terms of iterations, the loop ocaml script → web server test is faster than ocaml program → compilation → web server test.

How to install Vif

Vif is available on opam. You can install it using the command:

$ opam install vif

You can also obtain the development version of Vif via:

$ opam pin add https://git.robur.coop/robur/vif.git

The Vif project is hosted on our server git.robur.coop. You can also access a mirror on GitHub. Issues and pull requests can be proposed on both repositories (pull requests will be merged into our cooperative's repository).

My first Vif application

To understand Vif, we will iterate several times on a small project whose goal is to implement a website with a user area. This goal will allow us to see the main features of Vif. To do this, we will also use hurl, an HTTP client in OCaml that uses the same libraries as Vif. The latter will allow us to inspect a whole host of details regarding requests, responses and cookies.

Let's start by installing vif and hurl using opam. Next, we will create a folder and work in it to develop our brand new website.

$ opam install vif
$ opam install hurl
$ mkdir my-first-vif-application
$ cd my-first-vif-application

Vif is a library, but we also offer a small program that allows you to run an OCaml script and launch an HTTP server. This approach has the advantage of avoiding the "development, compilation, testing" loop (as is the case with native languages) and focusing primarily on development and testing.

We will come back to the vif tool, the library and the different ways of using this project in more detail later, but for now let's focus on developing our website.

My first webpage with Vif

We are therefore going to create an OCaml script in which we will develop our website:

$ cat >main.ml <<EOF
#require "vif" ;;

let () = Miou_unix.run @@ fun () -> Vif.run [] () ;;
EOF
$ vif --pid vif.pid main.ml &
$ hurl http://localhost:8080/
HTTP/1.1 404 Not Found

connection: close
content-length: 120
content-length: 120
content-type: text/plain; charset=utf-8

Unspecified destination / (GET):
user-agent: hurl/0.0.1
host: localhost
connection: close
content-length: 0
$ kill -SIGINT $(cat vif.pid)

Here, we write a new file called main.ml, which will simply launch the Miou scheduler and the Vif server. We then run this script using vif and make an HTTP request using hurl to obtain a response. Finally, we kill the Vif server.

The Vif server responded with a 404 (not found) response because no routes are defined, and it also describes the request it just received. We will therefore add a route for our website.

#require "vif" ;;

let index req _server () =
  let open Vif.Response.Syntax in
  let str = "Hello World!\n" in
  let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
  let* () = Vif.Response.with_string req str in
  Vif.Response.respond `OK
;;

let routes =
  let open Vif.Uri in
  let open Vif.Route in
  [ get (rel /?? any) --> index ]
;;

let () = Miou_unix.run @@ fun () ->
  Vif.run routes () ;;
$ vif --pid vif.pid main.ml &
$ hurl https://localhost:8080/
HTTP/1.1 200 OK

connection: close
content-length: 13
content-type: text/plain; charset=utf-8

Hello World!
$ kill -SIGINT $(cat vif.pid)

And here is our first page with Vif! We have added a new index function that will process the request and provide a response. In this response, we will write "Hello World!" and send it with the code 200 (`OK). We can see that this is indeed what the server responds when we use hurl.

This index function will be associated with a route get (rel /?? any). This route allows us to filter requests and specify that the index function will only process GET requests with the path "/".

Routes

The principle behind routes is fairly simple to understand: it allows you to associate (-->) a path on your website with a function that will process the request and respond. What Vif brings to the table is the ability to type routes. Another frequently requested route feature is the ability to specify holes in the path, which would be values provided by the user.

For example, we would like a route in which the user could specify the name:

#require "vif" ;;

let hello req (name : string) _server () =
  let open Vif.Response.Syntax in
  let str = Fmt.str "Hello %S!\n" name in
  let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
  let* () = Vif.Response.with_string req str in
  Vif.Response.respond `OK
;;

let user req (uid : int) _server () =
  let open Vif.Response.Syntax in
  let str = Fmt.str "User %d!\n" uid in
  let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
  let* () = Vif.Response.with_string req str in
  Vif.Response.respond `OK
;;

let routes =
  let open Vif.Uri in
  let open Vif.Route in
  [ get (rel / "hello" /% string `Path /?? any) --> hello
  ; get (rel / "user" /% int /?? any) --> user ]
;;

let () = Miou_unix.run @@ fun () ->
  Vif.run routes () ;;
$ hurl http://localhost:8080/hello/robur -p b
Hello "robur"!
$ hurl http://localhost:8080/user/42 -p b
User 42!
$ hurl http://localhost:8080/user/robur -p b
Unspecified destination /user/robur (GET):
...

As we can see, it is possible to define "holes" in our routes with Vif and obtain their value in the functions associated with these routes. It is also possible to type these holes so that you only process a certain type of information, such as an integer (as is the case for our second user function). Vif will then not only recognise integers, but also transform the value into a real OCaml integer that you can manipulate.

It is possible to define more complex "holes" that must match a regular expression. Here is an example where we would like to recognise certain fruits in our route.

#require "vif" ;;

type fruit =
  | Apple
  | Orange
  | Banana
;;

let pp ppf = function
  | Apple -> Fmt.string ppf "Apple"
  | Orange -> Fmt.string ppf "Orange"
  | Banana -> Fmt.string ppf "Banana"
;;

let fruit =
  let v = Tyre.regex Re.(alt [ str "apple"; str "orange"; str "banana" ]) in
  let inj = function
    | "apple" -> Apple
    | "orange" -> Orange
    | "banana" -> Banana
    | _ -> assert false in
  let prj = function
    | Apple -> "apple"
    | Orange -> "orange"
    | Banana -> "banana" in
  Vif.Uri.conv inj prj v
;;

let like req (fruit : fruit) _server () =
  let open Vif.Response.Syntax in
  let str = Fmt.str "I like %a!\n" pp fruit in
  let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
  let* () = Vif.Response.with_string req str in
  Vif.Response.respond `OK
;;

let routes =
  let open Vif.Uri in
  let open Vif.Route in
  [ get (rel /% fruit /?? any) --> like ]
;;

let () = Miou_unix.run @@ fun () ->
  Vif.run routes () ;;
$ hurl http://localhost:8080/orange -p b
I like Orange!
$ hurl http://localhost:8080/cherries -p b
Unspecified destination /cherries (GET):
...

Here we describe how to recognise certain fruits using a regular expression (and thanks to the re library). Next, we describe how to convert recognised strings into OCaml values (inj ). Finally, we use this new value in our route in order to recognise only the specified fruits.

Next steps

At this point, you should have a basic understanding of how routes work with Vif. The next step is to create our user space. This essentially consists of two routes:

  • a route for submitting user credentials to the server
  • a route reserved for logged-in users

First, we will focus on the server-side mechanics of credential verification. These credentials will initially be submitted using JSON.

Next, we will introduce the concept of cookies and middleware, which will allow us to create our second route (accessible only to logged-in users).

Finally, we will enhance this foundation with other features offered by Vif.

My basic userspace

Vif implements the basics of web frameworks. It offers middleware and cookie features, among others. In this chapter, we will use Vif's routing system to see how to implement a user space. We will essentially create two routes:

  • a route that allows users to submit their login details
  • a route that should only be accessible to users

As for the first route, since it involves submitting identifiers, it will be a POST route. In the previous chapter, it was possible to type routes, but Vif is also capable of typing the content of requests. In this case, we will create a POST route that expects content in JSON format (which will be the user's identifiers).

jsont & Vif

Vif uses the jsont library to obtain a DSL for describing information in JSON format. The aim here is to describe a type (the identifiers) and describe its equivalent in JSON format using jsont.

type credentials =
  { username : string 
  ; password : string }
;;

let credentials =
  let open Jsont in
  let username = Object.mem "username" string in
  let password = Object.mem "password" string in
  let fn username password =
    { username; password } in
  Object.map fn
  |> username
  |> password
  |> Object.finish
;;

Thanks to the credentials value, we can now serialise and deserialise JSON and obtain an OCaml value of type credentials. Vif is capable of handling this type of value in order to deserialise the content of a request itself as soon as it recognises the content type application/json.

let login req _server () =
  let open Vif.Response.Syntax in
  match Vif.Request.of_json req with
  | Ok (v : credentials) ->
    let str = Fmt.str "username: %S, password: %S\n"
      v.username v.password in
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let* () = Vif.Response.with_string req str in
    Vif.Response.respond `OK
  | Error (`Msg msg) ->
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let str = Fmt.str "Invalid JSON: %s\n" msg in
    let* () = Vif.Response.with_string req str in
    Vif.Response.respond (`Code 422)
;;

let routes =
  let open Vif.Uri in
  let open Vif.Route in
  let open Vif.Type in
  [ post (json_encoding credentials) (rel /?? nil) --> login ]
;;

let () = Miou_unix.run @@ fun () ->
  Vif.run routes () ;;

Here's how to send a JSON request using hurl:

$ hurl -m POST http://localhost:8080/ username=robur password=42 -p b
username: "robur", password: "42"
$ hurl -m POST http://localhost:8080/ foo=bar
HTTP/1.1 422 

connection: close
content-length: 122
content-type: text/plain; charset=utf-8

Invalid JSON: Missing members in object:
 password
 username

In this example, we have specified that our route expects content in JSON format. Next, Vif uses jsont to attempt to deserialise the given JSON to the OCaml value credentials. If it fails, we return code 422; otherwise, we display the information.

The goal here is that serialisation and deserialisation can be seen as a fairly repetitive task for the user. In this case, Vif handles two formats: JSON using jsont and multipart/form-data, which we will see in another chapter. These are the two most commonly used formats with the HTTP protocol, and Vif therefore handles these formats natively, providing you with a simple way to deserialise them into OCaml values.

Vif and cookies

When a user logs in, we want to keep the information on the user's side so that they remain logged in. We therefore want to store this information and also secure it. We will use Vif to create a new cookie, which will contain a JSON Web Token (JWT) that ensures the information is secure.

The JWT requires a secret, a value that only the server knows in order to encrypt/decrypt the JWT. This is where we introduce three concepts:

  • Vif's ability to load an external library (we will use jwto, an implementation of JWT in OCaml)
  • the ability to obtain configuration values (often found in the .env file of a website)
  • the creation of a new cookie if the user has logged in successfully
#require "jwto" ;;

type env =
  { secret : string }
;;

let verify { username; password } =
  match username, password with
  | "robur", "42" -> true
  | _ -> false
;;

let login req server { secret } =
  let open Vif.Response.Syntax in
  match Vif.Request.of_json req with
  | Ok ({ username; _ } as v) when verify v ->
    let token = Jwto.encode HS512 secret [ "username", username ] in
    let token = Result.get_ok token in
    let* () = Vif.Cookie.set ~name:"my-token" server req token in
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let* () = Vif.Response.with_string req "Authenticated!\n" in
    Vif.Response.respond `OK
  | Ok _ ->
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let* () = Vif.Response.with_string req "Unauthorized!\n" in
    Vif.Response.respond `Unauthorized
  | Error (`Msg msg) ->
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let str = Fmt.str "Invalid JSON: %s\n" msg in
    let* () = Vif.Response.with_string req str in
    Vif.Response.respond (`Code 422)
;;

let routes =
  let open Vif.Uri in
  let open Vif.Route in
  let open Vif.Type in
  [ post (json_encoding credentials) (rel / "login" /?? nil) --> login ]
;;

let () = Miou_unix.run @@ fun () ->
  let env = { secret= "deadbeef" } in
  Vif.run routes env ;;

As you can see, it is quite simple to load a new library; just use the require directive. Next, define a new env type that will contain the secret needed to generate JWTs.

We implement a fairly simple function verify that recognises a single user with their password.

Finally, we modify the login function to verify the information provided by the client. If the credentials are correct, we generate a JWT token using our secret passed to our login function and use Vif.Cookie.set to add a new cookie.

This is where you can specify the last argument of the Vif.run function. This value will be passed to all your handlers. Its main purpose is to allow developers to define their own type containing global information such as the secret used to generate JWTs. Of course, you can extend the env type. It is often compared to .env found in several web frameworks.

This is now the result when attempting to log in:

$ hurl -m POST http://localhost:8080/login username=robur password=21 -p rb
HTTP/1.1 401 Unauthorized

Unauthorized!
$ hurl -m POST http://localhost:8080/login username=robur password=42
HTTP/1.1 200 OK

connection: close
content-length: 15
content-type: text/plain; charset=utf-8
set-cookie: __Host-my-token=ALeeJoP8W2KfmX9oYcHMnjeJDuGJmV67brUluoEJgHLZWHEk...
  Path=/; Secure; HttpOnly; SameSite=Lax

Authenticated!

We are well connected! We can clearly see the cookie that the client should save. If we take a step back from the code, what we are doing here is managing a POST request containing identifiers, checking that they are correct, and creating a JWT using jwto if they are. In this token, we will simply store the user's username. We then need to inform the client that we would like to save this token on their end. We do this using Vif.Cookie.set (there are a whole host of options, but I'll leave you to discover them in the documentation).

As a client, we can see the cookie (with Set-Cookie). It is encrypted... twice! Once by Vif itself (you can — and should — specify the encryption key with Vif.config) and once by jwto.

We can now introduce a new concept in Vif: middleware.

Vif and middleware

Middleware is a simple function that applies to all requests (whether or not there is a defined route). Within this function, it is possible to inspect the request, particularly the header. However, it is impossible to:

  1. inspect the content of the request
  2. send a response

The purpose of middleware is to transform and/or add information from the header of the incoming request. For example, it is possible to analyse the cookies sent by the user and determine whether or not they were able to log in. Finally, this information is added to the request.

After the middleware, the handler associated with the route can finally process the request to which the middleware has added information. This information can be retrieved and a response can be sent accordingly.

The goal here would be to have middleware that attempts to deserialise the JWT present in the cookies. If it succeeds, it means that the person has logged in previously. The user's username is included in this token, which allows us to identify them. Here is how to create middleware using Vif.

type user =
  { username : string }
;;

let jwt =
  Vif.Middleware.v ~name:"jwt" @@ fun req target server { secret } ->
  match Vif.Cookie.get server req ~name:"my-token" with
  | Error _err -> None
  | Ok token ->
    let ( let* ) = Option.bind in
    let token = Jwto.decode_and_verify secret token in
    let* token = Result.to_option token in
    let* username = List.assoc_opt "username" (Jwto.get_payload token) in
    Some { username }
;;

let index req server _env =
  let open Vif.Response.Syntax in
  match Vif.Request.get jwt req with
  | None ->
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let* () = Vif.Response.with_string req "Unauthorized!\n" in
    Vif.Response.respond `Unauthorized
  | Some { username } ->
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let str = Fmt.str "Connected as %S!\n" username in
    let* () = Vif.Response.with_string req str in
    Vif.Response.respond `OK
;;

let routes =
  let open Vif.Uri in
  let open Vif.Route in
  let open Vif.Type in
  [ post (json_encoding credentials) (rel / "login" /?? nil) --> login
  ; get (rel /?? nil) --> index ]
;;

let () = Miou_unix.run @@ fun () ->
  let env = { secret= "deadbeef" } in
  let middlewares = Vif.Middlewares.[ jwt ] in
  Vif.run ~middlewares routes env ;;

Here, we create a new middleware called jwt. Its purpose is to search for a cookie called my-token (the one we created in the login handler) and decode (and verify) the JWT. If successful, we should obtain the username of the current user. We will simply add this information to the current request and return Some <data>.

In our new index handler, we will try to see if our middleware has returned the information correctly. If so, it means that the client has a JWT and is logged in. All we need to do is display the username!

Finally, we need to tell Vif that we want to apply this middleware.

Note that the middleware also has access to our env and therefore also has access to our secret value (required for jwto). The user can also specify the type of value that the middleware is capable of creating. In our case, we have defined the type user containing only the field username. We can of course extend this type with other values such as the user's ID, age, etc. In short, Vif allows you to define your own types.

You can now test our website and its user area:

$ curl -X POST http://localhost:8080/login \
  --header 'Content-Type: application/json' \
  --data "{ \"username\": \"robur\", \"password\": \"42\" }" \
  --cookie-jar cookies.txt
Authenticated!
$ curl http://localhost:8080/ --cookie cookies.txt
Connected as "robur"!
$ curl http://localhost:8080/
Unauthorized!

Et voilà! A user space created using Vif. This allowed us to explore the concepts of cookies, JWTs, and middleware.

At this stage, we have most of what a framework can offer. We can manage incoming information (and type it) as specified in our routes or in the request content, and process this information while allowing developers to specify their own types.

The main idea is that OCaml, being a typed language, has the advantage of characterising information (at least, much more than a simple string). We can use ADTs or records, which are much easier to use. It is then essentially a matter of defining how to convert untyped information into OCaml values.

Next steps

We now have a solid foundation for improving our website. In the next chapter, we will propose a way to display an HTML page for logging in, which will allow us to introduce a new format: multipart/form-data. Next, we will see how to generate HTML dynamically depending on whether the user is logged in or not. This will allow us to introduce new concepts such as ppx and static files.

My enhanced userspace

We have seen the basic elements for our user space. However, our website is still quite limited, as it assumes that the user knows how to send a POST request via the command line. In this chapter, we will improve our user space so that:

  • we have an HTML page with a login form
  • we can handle the multipart/form-data format rather than the JSON format
  • we have a dynamic HTML page depending on whether the user is logged in or not

Static HTML files (and conan)

When creating a website, we often want to deliver static content such as images or HTML pages. When sending these documents to the client, important information is sent along with them: the MIME type.

This informs the user of the type of content being sent (e.g. whether it is a *.png or *.jpg image).

With this in mind, we have developed software that can recognise the MIME type of any file: the conan project. Recognition is not based on the file extension, but rather on the content and a database that references most MIME types.

$ conan.file --mime login.html
text/html

Next, for Vif, we need to add a handler that, depending on the path given by the request, not only attempts to find the associated static file, but also recognises its MIME type using conan to deliver it to the client. Let's start with this HTML form:

<html>
<body>
<form action="/login" method="post" enctype="multipart/form-data">
  <label for="username">Enter your username: </label>
  <input type="text" name="username" required />
  <label for="password">Enter your password: </label>
  <input type="password" name="password" required />
  <input type="submit" value="Login!" />
</form>
</body>
</html>

Since the route for delivering a static file depends on the filename, we will introduce a new concept to Vif: default handlers. When Vif cannot find any routes for a request, it will execute a series of default handlers until one produces a response.

For static files, Vif already provides such a handler: Vif.Handler.static. Just add it to Vif.run:

let () = Miou_unix.run @@ fun () ->
  let env = { secret= "deadbeef" } in
  let middlewares = Vif.Middlewares.[ jwt ] in
  let handlers = [ Vif.Handler.static ?top:None ] in
  Vif.run ~handlers ~middlewares routes env ;;

The top value corresponds to the location of the static files (so that Vif prohibits access to other files deeper in the directory tree). In addition, our login.html file is in the same folder as our main.ml file (however, it is recommended to create an appropriate folder containing only static files).

$ hurl http://localhost:8080/login.html
HTTP/1.1 200 OK

transfer-encoding: chunked
etag: 3b8eae63b7baa4a7c24bfd8ee7600ee4b97306064e9ea336fca949011058a559
content-length: 356
content-type: text/html

<html>
<body>
<form action="/login" method="post" enctype="multipart/form-data">
  <label for="username">Enter your username: </label>
  <input type="text" name="username" required />
  <label for="password">Enter your password: </label>
  <input type="password" name="password" required />
  <input type="submit" value="Login!" />
</form>
</body>
</html>

We are now able to deliver static files! Note the appearance of ETag. This is information that allows the client to cache the file (and avoid re-downloading the content). Finally, note that conan has correctly recognised the MIME type of our file.

MIME-type and conan

MIME file recognition is quite difficult and is a topic in itself (in terms of performance and ability to recognise strange files). Even though conan finds quite a few solutions, it may happen that we are unable to recognise the file type. There are other, less obvious ways to transfer the contents of a file using Vif (such as Vif.Response.with_file), where you can specify the MIME type manually.

It is also possible to improve conan (and its database) to recognise a subset or larger set of files. We will leave it up to the user to choose the best solution for their context.

Vif & multipart/form-data

In our form, we specify that we would like to transfer the information in multipart/form-data format. This is a somewhat special format that is often used for websites. Fortunately, there is an implementation in OCaml that Vif uses: multipart_form.

Vif extends this library so that it is as easy to use as jsont: we therefore offer another DSL for describing the format of your forms:

let login_form =
  let open Vif.Multipart_form in
  let fn username password =
    { username; password } in
  record fn
  |+ field "username" string
  |+ field "password" string
  |> sealr
;;

let login req server { secret } =
  let open Vif.Response.Syntax in
  match Vif.Request.of_multipart_form req with
  | Ok ({ username; _ } as v) when login v ->
    let token = Jwto.encode HS512 secret [ "username", username ] in
    let token = Result.get_ok token in
    let str = "Authenticated!\n" in
    let* () = Vif.Cookie.set ~name:"my-token" server req token in
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let* () = Vif.Response.with_string req str in
    Vif.Response.respond `OK
  | Ok _ ->
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let* () = Vif.Response.with_string req "Unauthorized!\n" in
    Vif.Response.respond `Unauthorized
  | Error _ ->
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let str = Fmt.str "Invalid multipart/form-data\n" in
    let* () = Vif.Response.with_string req str in
    Vif.Response.respond (`Code 422)
;;

let routes =
  let open Vif.Uri in
  let open Vif.Route in
  let open Vif.Type in
  [ post (m login_form) (rel / "login" /?? nil) --> login
  ; get (rel /?? nil) --> index ]
;;

The idea is almost the same as with jsont. We just need to redefine our login route with m instead of json_encoding so that Vif accepts requests with a Content-Type of multipart/form-data.

$ hurl -m POST http://localhost:8080/login --multipart username=robur password=42 -p b
Authenticated!

Vif, tyxml and ppx

It would now be more useful to transmit HTML content rather than simple text. Several solutions are available, but we will use one in particular that emphasises the typed generation of an HTML document using TyXML:

#require "tyxml-ppx" ;;
#require "tyxml" ;;

open Tyxml ;;

let%html index username = {html|
<html>
<head><title>My Vif Website!</title></head>
<body>
  <p>Hello |html} username {html| !</p>
</body>
</html>
|html} ;;

let index username : Tyxml_html.doc = index [ Html.txt username ] ;;

let index req server _env =
  let open Vif.Response.Syntax in
  match Vif.Request.get jwt req with
  | None ->
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let* () = Vif.Response.with_string req "Unauthorized!\n" in
    Vif.Response.respond `Unauthorized
  | Some { username } ->
    let* () = Vif.Response.with_tyxml req (index username) in
    Vif.Response.respond `OK
;;

What's interesting here is that TyXML will check the HTML content to see if it complies with standards. Furthermore, if you remove the <head> tag, for example, TyXML will complain that this mandatory tag is missing.

This is not, of course, the only way to deliver content to the client. We can also respond with JSON as plain text, as we have done since the beginning of this book. This example essentially shows that it is possible to use ppx within a Vif script.

Vif & redirections

One last small detail, but one that may be important: users should now be redirected to the main page if they manage to log in, rather than receiving a message saying that they are logged in. Vif offers a redirect_to function that allows you to redirect the user to a given Vif.Uri.t.

let login req server { secret } =
  let open Vif.Response.Syntax in
  match Vif.Request.of_multipart_form req with
  | Ok ({ username; _ } as v) when login v ->
    let token = Jwto.encode HS512 secret [ "username", username ] in
    let token = Result.get_ok token in
    let str = "Authenticated!\n" in
    let* () = Vif.Cookie.set ~name:"my-token" server req token in
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let* () = Vif.Response.with_string req str in
    Vif.Response.redirect_to req Vif.Uri.(rel /?? any)
  | Ok _ ->
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let* () = Vif.Response.with_string req "Unauthorized!\n" in
    Vif.Response.respond `Unauthorized
  | Error _ ->
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let str = Fmt.str "Invalid multipart/form-data\n" in
    let* () = Vif.Response.with_string req str in
    Vif.Response.respond (`Code 422)
;;

We can now test our user space with a single command (since hurl handles redirection and cookies):

$ curl http://localhost:8080/login -F username=robur -F password=42 -L --cookie-jar cookies.txt
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head><title>My Vif Website!</title></head>
<body>
  <p>Hello robur !</p>
</body>
</html>

We're starting to get somewhere. We'll continue using the command line, but we can now test our website with our web browser! We're producing HTML content, and all we need to do is log in to http://localhost:8080/login.html to be redirected to our main page!

Next steps

At this point, we can consider that the hardest part is done. However, we would surely like to go further and, in particular, communicate with a database rather than our simple verify function. In the next chapter, we will see how to extend our user space so that we can use a persistent database to which our users can register.

A persistent userspace

Vif is therefore a web framework, but it does not provide any means of communicating with a database. However, when developing a website, it is essential to be able to store information (such as user data) persistently and independently of our HTTP server. Fortunately, another project allows communication with a database: caqti.

In this chapter, we will see how to communicate with a database using caqti so that our users' information is stored outside the HTTP server and in a persistent manner. To simplify this chapter, we will use sqlite3: a simple file that represents our database (note that caqti can communicate with other types of databases such as PostGreSQL).

Create & use a database

Here we will create a simple database with a table called users containing the user's username and password.

$ sqlite3 vif.db <<EOF
CREATE TABLE users (uid INTEGER, username TEXT, password TEXT, PRIMARY KEY(uid));
.quit
EOF

Next, we need to modify our Vif script so that we can read this database. To do this, we will use caqti-miou, the caqti support with our Miou scheduler. We need to install it:

$ opam install caqti-miou caqti-driver-sqlite3

Next, we will need to explain to Vif how to create an instance that can communicate with our database. Vif uses the concept of devices, which are global instances available from our server instance and therefore available in all our request handlers.

These devices have the particularity of being domain-safe, meaning that two domains can request these devices in parallel. We therefore need to ensure that their manipulation is also domain-safe.

In this case, caqti-miou creates what is called a connection pool to the database. There is only one database, but several handlers can process SQL requests in parallel (and require a connection to the database). Thanks to caqti, we can obtain a CONNECTION to the database (in a domain-safe manner) and, from this connection, make an SQL query (such as SELECT).

So let's first see how to create a Vif device and how to use it:

#require "caqti-miou" ;;
#require "caqti-miou.unix" ;;
#require "caqti-driver-sqlite3" ;;

type env =
  { sw : Caqti_miou.Switch.t
  ; uri : Uri.t
  ; secret : string }
;;

let caqti =
  let finally pool = Caqti_miou_unix.Pool.drain pool in
  Vif.Device.v ~name:"caqti" ~finally [] @@ fun { sw; uri; _ } ->
  match Caqti_miou_unix.connect_pool ~sw uri with
  | Error err -> Fmt.failwith "%a" Caqti_error.pp err
  | Ok pool -> pool
;;

let users req server _ =
  let pool = Vif.Server.device caqti server in
  let sql =
    let open Caqti_request.Infix in
    Caqti_type.(unit ->! int) "SELECT COUNT(uid) FROM users" in
  let fn (module Conn : Caqti_miou.CONNECTION) = Conn.find sql () in
  match Caqti_miou_unix.Pool.use fn pool with
  | Ok n ->
    let open Vif.Response.Syntax in
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset= utf-8" in
    let* () = Vif.Response.with_string req (Fmt.str "%d user(s)!\n" n) in
    Vif.Response.respond `OK
  | Error err ->
    let open Vif.Response.Syntax in
    let str = Fmt.str "SQL error: %a" Caqti_error.pp err in
    let* () = Vif.Response.with_string req str in
    Vif.Response.respond `Internal_server_error
;;

let routes =
  let open Vif.Uri in
  let open Vif.Route in
  let open Vif.Type in
  [ post (m form) (rel / "login" /?? nil) --> login
  ; get (rel /?? nil) --> index
  ; get (rel / "users" /?? nil) --> users ]
;;

let () =
  Miou_unix.run @@ fun () ->
  Caqti_miou.Switch.run @@ fun sw ->
  let uri = Uri.make ~scheme:"sqlite3" ~path:"vif.db" () in
  let env = { sw; uri; secret= "deadbeef" } in
  let middlewares = Vif.Middlewares.[ jwt ] in
  let handlers = [ Vif.Handler.static ?top:None ] in
  let devices = Vif.Devices.[ caqti ] in
  Vif.run ~devices ~handlers ~middlewares routes env
;;

Here, we extend our env type to include the caqti switch and the uri to our database. We then create our caqti device, which we will finally pass to Vif.run (via the devices argument).

The users handler is an example of an SQL query that counts the number of users. As you can see, we can retrieve our connection pool via Vif.Server.device caqti. Finally, we need to use this connection pool to make an SQL query, but we suggest you refer to the caqti documentation.

$ hurl http://localhost:8080/users
HTTP/1.1 200 OK

connection: close
content-length: 11
content-type: text/plain; charset= utf-8

0 user(s)!

Our database is empty, but this query confirms that we did indeed run an SQL query to find out that it is empty! The client connected to our Vif server, the server connected to our database, retrieved the information, processed it, and then responded in text format that there are zero users.

Verify passwords

We will add a new user manually and improve our login function so that it uses our database:

$ echo -n "42" | sha256sum
73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049  -
$ sqlite3 vif.db <<EOF
INSERT INTO users (username, password) VALUES ('robur', '73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049');
.quit
EOF
$ hurl http://localhost:8080/users -p b
1 user(s)!

Here, we can clearly see that the addition of a new user is taken into account by our Vif server. It should be noted that we do not need to restart our Vif server to obtain this response. This information is now stored in the database, and our Vif server will simply (and each time) request the number of users. Now let's re-implement the login function:

#require "digestif.c" ;;
#require "digestif" ;;

type user =
  { uid : int
  ; username : string 
  ; password : Digestif.SHA256.t }
;;

let user =
  let open Caqti_template.Row_type in
  let sha256 =
    let encode hash = Ok (Digestif.SHA256.to_hex hash) in
    let decode hex = Ok (Digestif.SHA256.of_hex hex) in
    custom ~encode ~decode string in
  product (fun uid username password -> { uid; username; password })
  @@ proj int (fun (t : user) -> t.uid)
  @@ proj string (fun (t : user) -> t.username)
  @@ proj sha256 (fun (t : user) -> t.password)
  @@ proj_end
;;

let login server (c : credentials) =
  let pool = Vif.Server.device caqti server in
  let sql =
    let open Caqti_request.Infix in
    Caqti_type.(string ->* user)
      "SELECT * FROM users WHERE username = ?" in
  let fn (module Conn : Caqti_miou.CONNECTION) = Conn.collect_list sql c.username in
  match Caqti_miou_unix.Pool.use fn pool with
  | Ok [ user ] ->
      let h = Digestif.SHA256.digest_string c.password in
      Digestif.SHA256.equal h user.password
  | _ -> false
;;

let login req server { secret } =
  let open Vif.Response.Syntax in
  match Vif.Request.of_multipart_form req with
  | Ok (({ username; _ } : credentials) as c) when login server c ->
    let token = Jwto.encode HS512 secret [ "username", username ] in
    let token = Result.get_ok token in
    let* () = Vif.Cookie.set ~name:"my-token" server req token in
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let* () = Vif.Response.with_string req "Authenticated!\n" in
    Vif.Response.redirect_to req Vif.Uri.(rel /?? any)
  | Ok _ ->
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let* () = Vif.Response.with_string req "Unauthorized!\n" in
    Vif.Response.respond `Unauthorized
  | Error _ ->
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let str = Fmt.str "Invalid multipart/form-data\n" in
    let* () = Vif.Response.with_string req str in
    Vif.Response.respond (`Code 422)
;;

We can see that caqti offers (using Caqti_template) more or less the same DSL that we used with jsont or multipart_form. Using this DSL, we can describe how to deserialise an SQL value to an OCaml value, which is what we do with the user type. Next, we modify our first login function so that it makes the SQL query to search for our user.

Passwords are hashed using the SHA256 algorithm (thanks to the digestif library). We will therefore hash the value given by the user and compare it with what we have in the database.

Finally, the second login function changes very little; we just need to change when verify c to when login server c so that our function can obtain the caqti connection pool.

$ curl http://localhost:8080/login -F username=robur -F password=42 \
  --cookie-jar cookies.txt -L
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>My Vif Website!</title></head>
<body><p>Hello robur !</p></body>
</html>

We now have a user space that obtains user information from a database!

We would like to emphasise that throughout, we have attempted to type all information (the password is no longer a string but a hash, which is a type offered by digestif). The results of SQL queries are also typed using Caqti. The main idea is always to prefer OCaml values (which have undergone a whole series of checks upstream) to basic values that require a whole ceremony to validate them (and, above all, to avoid bugs).

We will now focus on creating a new user. However, at this stage, I believe you should be able to create such a page on your own.

Create a new user

We will now implement a new page that will be a registration form. This page will be associated with a POST route that will add the new user to our database and redirect the client to our index page. We will reuse everything we have just learned here. Let's start with the registration page registration.html:

<html>
<body>
<form action="/subscribe" method="post" enctype="multipart/form-data">
  <label for="username">Enter your username: </label>
  <input type="text" name="username" required />
  <label for="password">Enter your password: </label>
  <input type="password" name="password" required />
  <input type="submit" value="Subscribe!" />
</form>
</body>
</html>

Next, we will create a new POST route to register the user.

let subscribe req server _ =
  let pool = Vif.Server.device caqti server in
  let ( let* ) = Result.bind in
  let search =
    let open Caqti_request.Infix in
    Caqti_type.(string ->* user)
      "SELECT * FROM users WHERE username = ?" in
  let insert =
    let open Caqti_request.Infix in
    Caqti_type.(t2 string string ->. unit)
      "INSERT INTO users (username, password) VALUES (?, ?)" in
  let already_exists (c : credentials) (module Conn : Caqti_miou.CONNECTION) =
    let* user = Conn.collect_list search c.username in
    Ok (List.is_empty user == false) in
  let insert (c : credentials) (module Conn : Caqti_miou.CONNECTION) =
    let hash = Digestif.SHA256.digest_string c.password in
    let hash = Digestif.SHA256.to_hex hash in
    Conn.exec insert (c.username, hash) in
  let result =
    let* c = Vif.Request.of_multipart_form req in
    let* () =
      match Caqti_miou_unix.Pool.use (already_exists c) pool with
      | Ok true -> Error (`Already_exists c.username)
      | Ok false -> Ok ()
      | Error (#Caqti_error.t as err) -> Error err in
    match Caqti_miou_unix.Pool.use (insert c) pool with
    | Ok () -> Ok ()
    | Error (#Caqti_error.t as err) -> Error err in
  let open Vif.Response.Syntax in
  match result with
  | Ok () ->
      let* () = Vif.Response.empty in
      Vif.Response.redirect_to req Vif.Uri.(rel / "form.html" /?? any)
  | Error (`Already_exists username) ->
      let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
      let str = Fmt.str "%S already exists.\n" username in
      let* () = Vif.Response.with_string req str in
      Vif.Response.respond `Conflict
  | Error (#Caqti_error.t as err) ->
      let open Vif.Response.Syntax in
      let str = Fmt.str "SQL error: %a" Caqti_error.pp err in
      let* () = Vif.Response.with_string req str in
      Vif.Response.respond `Internal_server_error
  | Error (`Invalid_multipart_form | `Not_found _) ->
      let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
      let str = Fmt.str "Invalid multipart/form-data\n" in
      let* () = Vif.Response.with_string req str in
      Vif.Response.respond (`Code 422)
;;

let routes =
  let open Vif.Uri in
  let open Vif.Route in
  let open Vif.Type in
  [ post (m login_form) (rel / "login" /?? nil) --> login
  ; get (rel /?? nil) --> index
  ; get (rel / "users" /?? nil) --> users
  ; post (m login_form) (rel / "subscribe" /?? nil) --> subscribe ]
;;

In the code above, we reuse our login_form value, which describes our form. This is because the form on the register.html page is the same as the one on login.html.

Next, we check whether the user already exists. We therefore make an initial SQL query and, if the user does not exist, we make an INSERT query. Finally, we handle most error cases.

The code may be longer, but what is really interesting is how the result value is calculated. If we look more closely, this function essentially consists of SQL queries and returning errors in certain cases. This is where Vif lets the developer choose how to organise the project.

One solution is to create a library containing the routes, SQL queries and functions for displaying the results. But all this is outside the scope of Vif, which is primarily intended to facilitate processes specific to managing HTTP requests and producing responses.

We can now test our server:

$ hurl -m POST http://localhost:8080/subscribe --multipart username=foo password=bar
HTTP/1.1 303 See Other

location: /form.html
connection: close
content-length: 0
$ curl http://localhost:8080/login -F username=foo -F password=bar -L \
  --cookie-jar cookies.txt
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>My Vif Website!</title></head>
<body><p>Hello foo !</p></body>
</html>

Congratulations! You have created the basics of a user space using Vif. At this point, we recommend that you explore the world of OCaml to see how to develop a real web application. Vif focuses primarily on the server side, but there are of course other aspects of web development that Vif does not handle (often referred to as the front end).

Final steps

We will conclude this tutorial by creating a chat room. Vif is capable of managing websockets. This means that several authenticated clients can communicate with each other in a shared space. This will allow us to explore more complex but equally interesting features of Vif.

The current site can also be improved in many ways. For example, we can add an email address (validated by emile) and send a confirmation email (using sendmail) to complete the registration process.

We could also add an avatar for our users and allow them to upload an image that would be validated with conan and stored on our server.

In short, there are many possible ways to go at this stage, but the Vif documentation is quite comprehensive and will provide you with all the information you need.

A chatroom with websockets and JavaScript

There is one last way to communicate with our HTTP server: the WebSocket protocol. The advantage of this protocol is that the connection is full-duplex: throughout the entire process, the user can communicate with you and you can continuously communicate with them. It's like a persistent communication.

In short, a good example of the use of WebSockets is a chat room. We want users to be able to communicate with other users in real time. We will therefore initiate WebSocket connections between these users in order to multiplex the messages.

First, we will create a fairly basic page where users can send messages to each other:

<html>
<head>
  <meta charset="utf-8">
  <script type="text/javascript" defer="defer" src="chat.js"></script>
  <title>Chat room</title>
</head>
<body>
  <form id="send">
    <input type="text" name="msg" required />
    <input type="submit" value="Send!" />
  </form>
</body>
</html>

Here we add a file called chat.js, which will be the result of compiling an OCaml file using js_of_ocaml. This small JavaScript script has two objectives:

  1. connect to the server via WebSocket and receive messages to write on the page
  2. send messages as soon as the user clicks on the Send! button.

To implement all this, we will use brr, a library that interfaces JavaScript functions in OCaml:

let form_elem = Brr.El.find_first_by_selector (Jstr.v "#send") |> Option.get

let send_message ev_submit =
  let open Fut.Syntax in
  let _ = Jv.call (Brr.Ev.to_jv ev_submit) "preventDefault" [||] in
  let form_elem = Brr_io.Form.of_el form_elem in
  let form_data = Brr_io.Form.Data.of_form form_elem in
  let body = Brr_io.Fetch.Body.of_form_data form_data
  and credentials = Brr_io.Fetch.Request.Credentials.same_origin in
  let init = Brr_io.Fetch.Request.init
    ~body ~credentials ~method':(Jstr.v "POST") () in
  let req = Brr_io.Fetch.Request.v ~init (Jstr.v "http://localhost:8080/send") in
  Fut.await begin
    let* result = Brr_io.Fetch.request req in
    match result with
    | Ok resp when Brr_io.Fetch.Response.ok resp -> Fut.return ()
    | Ok resp -> print_endline "Error!"; Fut.return ()
    | Error _ -> print_endline "Error!"; Fut.return ()
  end @@ Fun.id

let on_message ev =
  let msg = Jv.Jstr.get (Brr.Ev.to_jv ev) "data" in
  let div = Brr.El.(div [txt msg]) in
  Brr.El.append_children (Brr.Document.body Brr.G.document) [div]

let () =
  let socket = Brr_io.Websocket.create (Jstr.v "http://localhost:8080/chat") in
  let target = Brr_io.Websocket.as_target socket in
  let event = Brr.Ev.Type.create (Jstr.v "message") in
  ignore (Brr.Ev.listen event on_message target);
  let target = Brr.El.as_target form_elem in
  ignore (Brr.Ev.listen Brr_io.Form.Ev.submit send_message target)

The code may seem complex, and we could write the equivalent in JavaScript, but let's stay in the world of OCaml. The goal here is to retrieve our form and be able to read what the user has written as soon as they press the "Send!" button.

Next, we initiate a WebSocket connection with our server (taking care to keep our cookies so that we can remain authenticated).

Finally, for each message received from the WebSocket, we will simply write it on the fly on the page.

The JavaScript code can be obtained in this way:

$ opam install brr js_of_ocaml
$ ocamlfind c -linkpkg -package brr chat.ml
$ js_of_ocaml a.out -o chat.js

That's all we need on the client side. We will now return to Vif to:

  1. propose a handler to the WebSocket protocol
  2. and create a new POST route to send messages

Our listeners on our Vif server

The idea behind our chatroom is quite simple. When someone connects to our server via websocket, we create a listener in the sense that the client will listen for any new messages.

This listener will be stored in a global value so that it can be retrieved from any request handlers. As such, access must be protected so that this global value is domain-safe.

There are three essential operations:

  1. create a listener
  2. send a message to all listeners (i.e. all connected clients)
  3. delete a listener
type chatroom =
  { make : unit -> int * string Vif.Stream.source
  ; send : string -> unit
  ; shutdown : int -> unit }
;;

let chatroom =
  let uid = Atomic.make 0 in
  let actives = Hashtbl.create 0x100 in
  let mutex = Miou.Mutex.create () in
  let make () =
    let n = Atomic.fetch_and_add uid 1 in
    Miou.Mutex.protect mutex @@ fun () ->
    let q = Vif.Stream.Bqueue.create 0x100 in
    Hashtbl.replace actives n q;
    n, Vif.Stream.Source.of_bqueue q in
  let shutdown uid =
    Miou.Mutex.protect mutex @@ fun () ->
    Hashtbl.remove actives uid in
  let send msg =
    Miou.Mutex.protect mutex @@ fun () ->
    let fn _ q = Vif.Stream.Bqueue.put q msg in
    Hashtbl.iter fn actives in
  let finally _ =
    Miou.Mutex.protect mutex @@ fun () ->
    let fn _ q = Vif.Stream.Bqueue.close q in
    Hashtbl.iter fn actives in
  Vif.Device.v ~name:"chatroom" ~finally [] @@ fun _ ->
  { make; send; shutdown }
;;

Here, we introduce two new concepts: a bounded queue and streams. We won't go into detail about these modules, but they allow information to be transmitted (and it's always domain-safe) between tasks. Conceptually, several tasks (probably dispatched across several domains) will run to listen for any messages we might want to send. A task will then appear that will execute the handler for the POST request (allowing messages to be sent) and will have to transmit this message to all active listeners (this is the purpose of the send function).

Our chatroom, being global to our server, will be a device. We will then create the WebSocket handler and a final route to be able to send messages.

Websocket

The WebSocket protocol is a protocol that can be initiated from an HTTP request. It involves creating a route and informing the client that we would like to switch to the WebSocket protocol rather than HTTP, which is called an upgrade. Vif allows you to attempt this upgrade. The client will then be redirected to another handler, the WebSocket handler. This handler is special because it no longer processes a request and provides a response, but works with a stream of inputs (ic) and a stream of outputs (oc).

let chat req server _ = Vif.Response.websocket ;;

let websocket ic oc server _ =
  let t = Vif.Server.device chatroom server in
  let uid, src = t.make () in
  let fn str = oc (`Msg (`Text, true), str) in
  let prm0 = Miou.async @@ fun () -> Vif.Stream.Source.each fn src in
  let prm1 = Miou.async @@ fun () ->
    let rec go () =
      match ic () with
      | None | Some (`Connection_close, _) -> oc (`Connection_close, String.empty)
      | Some _ -> go () in
    go () in
  let _ = Miou.await_first [ prm0; prm1 ] in
  t.shutdown uid
;;

Here, we introduce a few concepts related to Miou. When a client connects to our websocket, the goal is to create tasks that will work together:

  1. one will consume everything the client can send (and it should not send anything normally)
  2. the other task will consist of transmitting messages from our listener to our client

Vif.Stream.Source.each will execute fn as soon as the listener receives a message, and fn will simply write this message to the client using oc.

One or both of these tasks will stop (because the client has disconnected or because we want to shutdown the server). What is certain is that in any case, everything must end. Miou.await_first will wait for one of the tasks and, more importantly, will cancel the other. We can finally release the listener resource correctly.

It may be interesting to look at Miou at this point and how it manages tasks. We can recommend a short book that explains in detail what a scheduler looks like in OCaml 5 (with effects) and what Miou offers.

Send a message!

Here, we will create a new handler, which will be our last one and will summarise everything we have learned since the beginning of this short book. It is a handler for a POST request in which we would have our message (msg). It is still a question of whether the client is connected, and we will simply prefix the message with the client's name.

type msg = { msg : string } ;;

let msg =
  let open Vif.Multipart_form in
  let fn msg = { msg } in
  record fn
  |+ field "msg" string
  |> sealr
;;

let send req server _ =
  let open Vif.Response.Syntax in
  match Vif.Request.of_multipart_form req, Vif.Request.get jwt req with
  | Ok { msg }, Some { username }->
    let t = Vif.Server.device chatroom server in
    let str = Fmt.str "%s: %s\n" username msg in
    t.send str;
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let* () = Vif.Response.with_string req "Message sent!\n" in
    Vif.Response.respond `OK
  | Error _, _ ->
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let str = Fmt.str "Invalid multipart/form-data\n" in
    let* () = Vif.Response.with_string req str in
    Vif.Response.respond (`Code 422)
  | _, None ->
    let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
    let* () = Vif.Response.with_string req "Unauthorized!" in
    Vif.Response.respond `Unauthorized
;;

Note that we are reusing our chatroom device here. This does not involve creating a listener as in our WebSocket handler, but rather broadcasting the message to all listeners.

Mix them all!

We now need to properly configure our new chatroom device in Vif and give it the handler for WebSocket connections:

let routes =
  let open Vif.Uri in
  let open Vif.Route in
  let open Vif.Type in
  [ post (m login_form) (rel / "login" /?? nil) --> login
  ; get (rel /?? nil) --> index
  ; get (rel / "users" /?? nil) --> users
  ; post (m login_form) (rel / "subscribe" /?? nil) --> subscribe
  ; get (rel / "chat" /?? nil) --> chat
  ; post (m msg) (rel / "send" /?? nil) --> send ]
;;

let () =
  Miou_unix.run @@ fun () ->
  Caqti_miou.Switch.run @@ fun sw ->
  let uri = Uri.make ~scheme:"sqlite3" ~path:"vif.db" () in
  let env = { sw; uri; secret= "deadbeef" } in
  let middlewares = Vif.Middlewares.[ jwt ] in
  let handlers = [ Vif.Handler.static ?top:None ] in
  let devices = Vif.Devices.[ caqti; chatroom ] in
  Vif.run ~devices ~handlers ~middlewares ~websocket routes env
;;

There you go! You can now access the page http://localhost:8080/chat.html and, if you are logged in, you can send a message that others will be able to read. It's a real chat room made with Vif.

Conclusion

Of course, there is room for improvement (starting with the design!). But the bulk of the logic, the backend, is there. Despite Vif's minimalism, it is possible to achieve satisfactory results fairly quickly.

Vif offers a way to develop websites with OCaml by taking up the idea of OCaml scripts. Fortunately, this is not the central idea (note the use of effects with Miou, the possibility of parallelising request management with OCaml 5, etc.). The idea of scripting in OCaml is interesting because it requires very little to get a website up and running.

Finally, Vif attempts to offer, at all levels of the HTTP protocol, a way of typing information so that all checks can be performed upstream using DSLs such as jsont or caqti. The idea is to really take advantage of the OCaml type system (and see it more as an assistant rather than a constraint).