Post

Building a Website with OCaml and Dream – Part 1

Learn how to build a modular website using OCaml and Dream, starting with project setup, routing, and templating.

Building a Website with OCaml and Dream – Part 1

Updates

  • 2025-02-25: Add a workaround for an issue with pure Windows installations

Series

Introduction

I am an object-oriented programming (OOP) developer, and my primary working language is Ruby. However, I have always had a profound love for functional programming languages, especially OCaml. It was the first programming language I learned at school, though back then it was still Caml Light. Later, during my computer science studies in college, functional programming remained central to my education, and I continued exploring it through languages like Scheme and LISP. Eventually, OOP became my professional focus, but I never stopped longing for the functional side of programming.

To bridge the gap and reignite my passion for functional programming, I set myself two challenges:

  1. Dive deeply back into OCaml, evangelize it, (and perhaps even work professionally with it one day in a distant future!)
  2. Create approachable documentation and tutorials for functional programming languages, particularly OCaml. My goal is to demonstrate that anyone can use OCaml effectively through down-to-earth, step-by-step tutorials and practical blog posts—the kind of resources I wish I had when I started.

My Background

Besides being a Senior Software Engineer working mainly in Ruby and Go, I also dedicate a part of my working time teaching software engineering, which is why I hope I can bring something to the table with such a project.

The App

I love lists. I make lists of things I enjoy, things I’ve watched, books I’ve read, and games I’ve played. For this series of blog posts, I’ll create a very simple web app that allows me to share the games I’ve finished, when I finished them, and how much I enjoyed them.

Here’s the planned stack:

  • I’ll use Dream as the framework for the website.
  • I’ll experiment with incorporating some Ruby on Rails technology, like Hotwire, just to see if it’s feasible.
  • If I can use OCaml for frontend development, I’ll explore that as well.

The idea is to document this experiment and help others who may hesitate to try OCaml or face roadblocks they can’t overcome.

Acknowledgement

I’ve been an OOP programmer for many years, with Ruby as my main language. This means I’ve developed certain habits that may be difficult to set aside. Even within OOP, my views are often considered opinionated. When it comes to functional programming, I may take unconventional approaches, think outside the box, or adopt practices that are not standard in the functional programming world. I apologize in advance and invite you to share advice if you have expertise in this area—because I’m learning as I go.

Prerequisites

  • I’ll assume you have OPAM and Dune installed and working. This will be my only assumption about your setup.
  • I’ll assume you know the basics about OCaml syntax. I don’t know much more myself!
  • I chose Containers as my standard library

Hello World!

Creating the Project

To start, let’s use dune to create a new project:

1
dune init project quest_complete

commit 06b662d

This will create the following structure:

1
2
3
4
5
6
7
8
9
10
11
12
quest_complete/
├── _build/
├── bin/
│   ├── dune
│   └── main.ml
├── lib/
│   └── dune
├── test/
│   ├── dune
│   └── test_quest_complete.ml
├── dune-project
└── quest_complete.opam

Next, we’ll add the Dream library and some other dependencies. Run the following command to install them:

1
opam install containers dream

Here’s the content of the dune-project file with some basic information filled in:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(lang dune 3.17)

(name quest_complete)

(generate_opam_files true)

(source
 (github Lomig/quest_complete))

(authors "Lomig <lomig@hey.com>")

(maintainers "Lomig <lomig@hey.com>")

(license GPL-3.0-only)

(package
 (name quest_complete)
 (synopsis "Track and celebrate the games you’ve conquered.")
 (description "QuestComplete is your ultimate companion for tracking completed video games. Keep a record of your gaming victories, relive your favorite quests, and celebrate your progress in style. Level up your gaming organization with QuestComplete!")
 (depends ocaml containers dream)
 (tags
  ("website" "games" "completion")))

commit 85e5867

The following paragraph is for Windows (non-WSL) users only.

You may encounter a SSL error when trying to install the dependencies. To fix this, add this file to your project root:

1
2
3
4
5
6
7
8
(lang dune 3.17)

(context
 (default
  (paths
   (PATH
    "C:\\Users\\YOUR_USER_NAME\\AppData\\Local\\opam\\.cygwin\\root\\bin"
    (:standard \ "C:\\WINDOWS\\system32")))))

Setting Up a Basic Web Server

The Dream documentation is straightforward, so let’s configure it. Update the dune file for the executable:

1
2
3
4
(executable
 (public_name quest_complete)
 (name main)
 (libraries quest_complete dream))

Now, let’s create a minimal web server in main.ml:

1
2
3
4
5
6
7
let () =
  Dream.run
  @@ Dream.logger
  @@ Dream.router [
    Dream.get "/" (fun _ ->
      Dream.html ("hello world"));
  ]

This snippet does the following:

  1. Starts a Dream web server.
  2. Adds middleware to log server activity.
  3. Defines an array of routes.
  4. Sets up a GET route for the root (/) that returns an HTML response.

Routes follow this structure: Dream.VERB PATH -> (fun REQUEST -> RESPONSE)

Finally, build and run your project:

1
dune exec quest_complete

commit c9c110c

Organizing Code (MVC-ish Style)

To make the project more modular, we’ll structure it in an MVC-like way.

Router

We’ll organize server and client code into separate directories under lib. Let’s move the routes to a dedicated router.ml file inside the server directory. Update the dune files accordingly.

1
2
3
(library
 (name server)
 (libraries dream))
1
2
3
4
5
let routes =
  [
    Dream.get "/" (fun _ -> Dream.html ("hello world"));
  ]
;;

Update the main executable to use the new router:

1
2
3
4
(executable
 (public_name quest_complete)
 (name main)
 (libraries dream server router))
1
2
3
4
5
6
open Server

let () =
  Dream.run
  @@ Dream.logger
  @@ Dream.router Router.routes

Rebuild and run:

1
dune exec quest_complete

Everything should still work!

commit 73265f4

Handlers

Dream introduces the concept of handlers, which are essentially controllers. Let’s create a dedicated space for them under lib/server/handlers.

New files
1
2
3
(library
 (name handlers)
 (libraries dream))
1
2
(* pages will deal with static pages of the website *)
let homepage _ = Dream.html ("hello world")
Updates
1
2
3
(library
 (name server)
 (libraries dream handlers))
1
2
3
4
5
6
7
open Handlers

let routes =
  [
    Dream.get "/" Pages.homepage
  ]
;;

Check the server again:

1
dune exec quest_complete

commit 39d490a

Views

Views handle the interface with the user. We’ll move the presentation logic into a views directory.

New files
1
2
(library
 (name views))
1
let homepage = "hello world"
Updates
1
2
3
(library
 (name handlers)
 (libraries dream views))
1
let homepage _ = Views.Pages.homepage |> Dream.html

Rebuild and run the project:

1
dune exec quest_complete

commit cfa44de

Raw Templating

Mixing HTML with logic can get messy. Dream supports templating, which we’ll use to separate concerns.

New files
1
2
3
4
5
(library
 (name templates)
 (libraries dream))

(include_subdirs qualified)

In OCaml, each ml file transposes to a module; but as there will be no file directly in the templates directory and we need to nest sub-directories to be considered as nested modules, we use the (include_subdirs qualified) stanza.

Consider this folder structure:

1
2
3
4
5
6
7
templates/
├── foo/
│   ├── bar.ml
│   └── ber.ml
├── buzz/
│   └── bazz.ml
└── dune
  • if dune contains the stanza (include_subdirs qualified), we will get access to modules Templates.Foo.Bar, Templates.Foo.Ber and Templates.Buzz.Bazz
  • if dune contains the stanza (include_subdirs unqualified), we will get access to modules Templates.Bar, Templates.Ber and Templates.Bazz
1
let render _ = "hello world"
Changed files
1
2
3
(library
 (name views)
 (libraries templates))
1
let homepage = Templates.Pages.Homepage.render None

commit 00be062

Embedded ML Templating

I want to serve some HTML though, and for that I’ll use what Dreams provide: a templating system. The main idea is:

  • Writing some code mixing HTML and OCaml (think Moustache or ERB for example)
  • On build, having a preprocessor that would translate the template in pure HTML, wrapped up in OCaml functions which Dream can use.
  • We will rename templates/pages/homepage.ml to templates/pages/homepage.eml.html to mark it as being Embedded ML within HTML.
New files
1
2
3
4
5
(rule
 (targets homepage.ml)
 (deps homepage.eml.html)
 (action
  (run dream_eml %{deps} --workspace %{workspace_root})))

A rule will map deps to targets using the action.

  • deps are files in the current directory
  • targets will be generated in the _build directory on build
1
2
3
4
5
6
7
8
9
10
11
12
13
let render _ =
<!DOCTYPE html>
<html>

<head>
    <title>Homepage</title>
</head>

<body>
    <p>Hello world!</p>
</body>

</html>

Our usual check it still works:

1
dune exec quest_complete

commit 01bdf25

Layouts

To avoid duplicating boilerplate, create a layout template:

New files
1
2
3
4
5
(rule
 (targets main.ml)
 (deps main.eml.html)
 (action
  (run dream_eml %{deps} --workspace %{workspace_root})))
1
2
3
4
5
6
7
8
9
10
11
12
13
let layout ?(title="QuestComplete") content =
<!DOCTYPE html>
<html>

<head>
    <title><%s! title %></title>
</head>

<body>
    <%s! content %>
</body>

</html>
Changed files
1
2
let render _ =
<p>Hello world!</p>
1
let homepage = Templates.Layouts.Main.layout @@ Templates.Pages.Homepage.render None

commit 989cbb5

Now you have a static, basic website in OCaml! In the next part, we’ll explore database connections, add CSS, and more.

This post is licensed under CC BY 4.0 by the author.

Trending Tags