How to build disruptive OCaml microservices with BuckleScript
Recently, I started tinkering with OCaml. It’s Very Fun™, which makes it a great fit for a quick side project. So, for a few hours this past week, I decided to cram a bunch of different Very Fun™ things together to build a trivial web app. It went well — so here’s a tutorial on how I did it.
The Plan
We’ll be writing a few lines of OCaml, compiling it to JavaScript using BuckleScript, producing a .js file which runs a microservice using Micro.
“Fatigue!” you may claim. Yeah, that’s kinda the point. We’ll get our hands dirty with a variety of cool things, each of which can be further explored or ignored as you see fit — that was my plan, anyway.
The Tools
We’ll need a couple things from npm:
yarn add micro # or replace 'yarn' with 'npm install'
yarn add --dev bs-platform
Allow me to briefly explain what these two things are.
Micro is a super-tiny library for turning blocks of code like this…
module.exports = (req, res) => {
res.end('Welcome to Micro')
}
…into web-servers. Just a few lines, no need for any boilerplate or configuration, making our lives much easier as we attempt to compile another language into it.
BuckleScript is a toolchain developed at Bloomberg for compiling OCaml code into readable, performant JavaScript. It is incredibly powerful, and we’ll only be using a very, *very *small subset of its features, but it works quite well and is easy to get up and running.
Now a bit of config.
First we’ll need to tell BuckleScript where our files are (let’s make a new
src/
directory and just put things in there). To do this we create a
bsconfig.json
with the following two fields.
{
"name": "bucklescript-micro-example",
"sources": [
"src/"
]
}
Sa-weet — now let’s add some scripts to package.json
to make our lives easier.
{
"dependencies": {
"micro": "^8.0.1"
},
"devDependencies": {
"bs-platform": "^1.8.2"
},
"scripts": {
"build": "bsb",
"watch": "bsb -w",
"start": "node lib/js/src/index.js"
}
}
Our build
command will use the bsb
executable (provided to us from
bs-platform
) to build our code. watch
does the same thing, but will also
watch for any file changes as we develop and re-build them automagically.
Finally, start
will run our web server. The path afterwards is where
BuckleScript will put our compiled JavaScript.
The Code
So far so good, right? Now, we can start writing code. Let’s kick things off
with a simple function, just to get a feel for how BuckleScript works its magic.
Start by creating a file src/add.ml
and add the following:
let add a b = a + b
If we run npm run build
(or we can run npm run watch
and leave it in a
separate tab), we should see a brand new lib/
folder in our profile. Diving
in, we find lots of definitions, and the compiled output: lib/src/add.js
:
// Generated by BUCKLESCRIPT VERSION 1.8.2, PLEASE EDIT WITH CARE
'use strict';
function add(a, b) {
return a + b | 0;
}
exports.add = add;
/* No side effect */
Magic! Not only did we compile the code, but we’re exporting our add
function
(with the right name and all). We can now use our add
function, originally
written in OCaml, in node:
$ node
> require("./lib/js/src/add.js").add(5, 6)
11
Now the fun part — let’s try to write some code that uses micro.
The Bindings
We can write functions like add
ourselves, but in order to interface our code
with existing JS functions, we’ll need to dive into the world of
foreign function interface_s
_(also known as FFIs).
Simply put, FFIs let BuckleScript know:
- The type definitions of our foreign objects. This allows us to treat these
objects as first class citizens, passing them to and from other functions in
our codebase. (For example,
micro
will provide us with “request” and “response” objects. We can type these so later on we can write functions such asrenderIndexPage : res -> string -> unit
). - **What type of syntax our OCaml code should compile down to. **In other words,
should
fillStyle ctx "blue"
compile down toctx.fillStyle("blue")
orctx.fillStyle = "blue"
?
These bullet points will make more sense as we go along. For now, let me introduce what one of these bindings looks like.
type req
type res
type server
external micro : (req -> res -> string) -> server = "micro" [@@bs.module]
external listen : server -> int -> unit = "listen" [@@bs.send]
Let’s break this down.
- First we define a few types. Now we can create functions that consume/return a
“thing” of type
req
,res
, andserver
. external
is a keyword used for defining FFIs in OCaml. You’ll see this a lot when working with BuckleScriptmicro
andlisten
will correspond to functions we can now use in our OCaml code. Thanks to the type definitions next to them (after the colon), they are typesafe and will let your program compile (as well as make tooling such as merlin infinitely more useful).- The strings
"micro"
and"listen"
, somewhat confusingly, correspond to the JavaScript identifiers that BuckleScript will output. We can technically leave these out (and instead specify""
) since they are equal to the function names we are binding to. - Finally, the items in the square brackets (namely
bs.module
andbs.send
) let BuckleScript know what sort of JavaScript expression we want our newmicro
andlisten
functions to compile to.
I’d like to expand that last bullet point.
[@@bs.send]
This treats the first argument as a JS object and sends the remaining arguments as parameters.
external listen : server -> int -> unit = "listen" [@@bs.send]
(* ...other stuff... *)
listen thing_of_type_server 1337
Will result in (roughly) the following code:
thing_of_type_server.listen(1337)
[@@bs.module]
This attribute lets BuckleScript know that you are interfacing with a JS module,
adding a require
when necessary.
external add : int -> int -> int = "add" [@@bs.module]
external sub : int -> int -> int = "sub" [@@bs.module "coolpackage"]
let f = add 1 2;;
let g = sub 7 6;;
Results in:
var Add = require("add");
var Coolpackage = require("coolpackage");
var f = Add(1, 2);
var g = Coolpackage.sub(7, 6);
As an exercise to the reader: what do the attributes[@@bs.get]
,
[@@bs.set]
, and [@@bs.val]
do?
For more on FFI: refer to the official BuckleScript docs.
We now have access to two functions: micro
and listen
which are used in the
following ways:
micro
accepts a function (which accepts two arguments of type req
and res
respectively and returns a string
) and returns a server
.
let server = micro (fun req -> fun res -> "Hello, world!");;
listen
accepts a server
and an int
and returns a noop (type unit
).
listen server 1337;;
All together now!
type req
type res
type server
external micro : (req -> res -> string) -> server = "micro" [@@bs.module]
external listen : server -> int -> unit = "listen" [@@bs.send]
let server = micro (fun req -> fun res -> "Hello, world!");;
listen server 1337;;
If we place this code in src/index.ml
, running npm run build
will produce
lib/js/src/index.js
with the following contents:
// Generated by BUCKLESCRIPT VERSION 1.8.2, PLEASE EDIT WITH CARE
'use strict';
var Micro = require("micro");
var server = Micro((function (_, _$1) {
return "Hello, world!";
}));
server.listen(1337);
exports.server = server;
/* server Not a pure module */
Now let’s run npm start
and visit localhost:1337
.
Better yet, we can install now (npm install -g now
) and deploy our site instantly (simply by typing now
in our terminal).
And Voila! A “web-server” written in OCaml, compiled down to JavaScript. It’s not much, but it’s a straight spike through a variety of technologies. Hopefully you find one or two of ’em interesting, and I encourage you to continue playing and exploring.
Going Forward
Here are some more questions to ponder on.
- Using micro, the first argument represents an instance of
http.IncomingMessage
. This instance has aurl
property — how would we go about extracting the URL and displaying a different message? - If we surround
Hello, world!
with<strong></strong>
, we see that our browser renders an HTML document. Experiment with creating various “template” functions to build a Real Website™. (i.e.fun req -> fun res -> layout req
) - Instead of returning a string, use various methods on the
res
parameter, which is an instance ofhttp.ServerResponse
.
You may also be interested in Reason: a new syntax for OCaml developed at Facebook. It’s gaining a lot of traction in the JavaScript community, and even has React bindings! I’m personally a huge fan of my friend Jared’s recent (excellent) blog post about ReasonReact.
In part 2, We’ll explore @@bs.send.pipe
and how to better interface with
chainable JavaScript APIs:
Typesafe JavaScript Chaining with OCaml and BuckleScript
I hope this serves as a gentle introduction to one of my favorite things happening in JavaScript right now. Go forth and explore, and be sure to share what you create.