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.
Updates
- 2025-02-25: Add a workaround for an issue with pure Windows installations
Series
- Building a Website with OCaml and Dream – Part 1
- Building a Website with OCaml and Dream – Part 2
- Building a Website with OCaml and Dream – Part 3
- Building a Website with OCaml and Dream – Part 4
- Building a Website with OCaml and Dream – Part 5
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:
- Dive deeply back into OCaml, evangelize it, (and perhaps even work professionally with it one day in a distant future!)
- 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
Dreamas 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
OPAMandDuneinstalled and working. This will be my only assumption about your setup. - I’ll assume you know the basics about
OCamlsyntax. I don’t know much more myself! - I chose
Containersas 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
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")))
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:
- Starts a Dream web server.
- Adds middleware to log server activity.
- Defines an array of routes.
- Sets up a
GETroute 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
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!
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
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
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
dunecontains the stanza(include_subdirs qualified), we will get access to modulesTemplates.Foo.Bar,Templates.Foo.BerandTemplates.Buzz.Bazz - if
dunecontains the stanza(include_subdirs unqualified), we will get access to modulesTemplates.Bar,Templates.BerandTemplates.Bazz
1
let render _ = "hello world"
Changed files
1
2
3
(library
(name views)
(libraries templates))
1
let homepage = Templates.Pages.Homepage.render None
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
Dreamcan use. - We will rename
templates/pages/homepage.mltotemplates/pages/homepage.eml.htmlto 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.
depsare files in the current directorytargetswill be generated in the_builddirectory 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
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
Now you have a static, basic website in OCaml! In the next part, we’ll explore database connections, add CSS, and more.