Work in progress web server framework for BuckleScript/ReasonML that works
directly on top of the native http
module in node.js.
Idiomatic strongly typed FP code is prioritized over run-time performance.
This project is in an exploratory state, the goal right now is to find a great API. So a lot of things might change
Currently, if you want to write a bucklescript-based back-end, the most viable
option is to use bucklescript bindings to existing node.js frameworks, like express
.
Unfortunately, express is not geared towards functional programming paradigms,
e.g. it is a common practice to mutate the express Request
object.
The philosophy of this framework is that a middleware doesn't mutate the context, but passes data on to the next middleware using monadic composition.
This library is work in progress, and the following only represents an intent of the usage of this library.
let findUser = (id, request) => {
userRepository.find(id)
|> Async.bind(fun
| None => Response.render(~status=404, ())
| Some(user) => Response.next(user)
)
}
let renderUser = (user, request) => {
Response.render(~contentType="application/json", encodeUser(user));
}
let updateUser = (id, userJson, request) => {
userRepository.update(id, userJson)
|> Async.bind(fun
| Ok(user) => Response.render( /* ... */ )
| Error(UserNotFound) => Response.render(~status=404)
| _ => Response.render(~status=500)
)
}
let getBodyJson = (request) => {
// Retrieve the body stream and return as a Js.Json.t
}
let apiRouter = router [
get("/users") >=> renderAllUsers,
get("/users/:id") >=> decodePathParam("id") >=> findUser >=> renderUser,
post("/users/:id") >=> decodePathParam("id") >=> userId => getBodyJson >=> decodeUser >=> updateUser(userId)
]
// Alternatively
let apiRouter = router [
path("/users") >> router [
get("/") >> renderAllUsers,
path(":/id") >> decodePathParam("id") >>= userId => router [
mehod("GET") >> findUser(userId) >>= renderUser,
method("POST") >> getBodyJson >>= decodeUser >>= updateUser(userId)
]
]
]
let application = router [
path("/api") >> apiRouter,
// ensureAuthentication would inspect authentication tokens, etc, and
// pass the relevant data to the next layer
path("/private") >> ensureAuthentication >>= privateRouter
]
Requests are handled by a Handler.t
which has the signature:
type t('a) = (Request.t, Response.t) => Async(HandlerResult('a))
So the HandlerResult
can carry information. This could be looking up the
value of a cookie.
The type async
represents a result that will be evaluated asynchronously. The
actual type is a function that accepts TWO callbacks, one for successful result,
and one to handle if an exception was thrown.
type async('a) = (('a => unit, exn => unit)) => unit
Exceptions here are only intended for truly exceptional conditions, e.g. a broken database connection.
type httpResult('a) =
| CannotHandle
| Done(Response.t)
| Continue('a)
| NewContext('a, Request.t, Response.t);
CannotHandle
This handler is unable to handle the request. E.g. a handler of httpGET
requests would return this when called with an HTTPPOST
request.Done
A response has been generatedContinue
The handler was successful but have not generated a response. The next handler in the chain should be called. This can carry a payload, e.g. the value of a cookie.NewContext
LikeContinue
but with some changes to theRequest
(we may have filtered on a subpath) orResponse
(We may have added a cookie/response header)
Handlers can be directly composed using the >>
operator. This is most usable
when dealing with handlers that that don't carry values in the return type, e.g.
request filtering:
path("/foo") >> method("GET") >> router([ ... ])
This is the monadic bind operator, which is usable when a handler carries a value:
tryGetCookie("username")
>>= cookie => switch(cookie) {
| Some(userName) => sendText("Hello, " ++ userName);
| None => sendText("Hello, mysterios one");
})
This is the kleisli composition of handler-generating functions:
let ( >=> ): ('a => t('b), 'b => t('c)) => ('a => t('c))
let getUser: (string => Handler.t(user));
let renderUser: (user => Handler.t(unit));
let getAndRenderUser: (string => Handler.t(unit)) = getUser >=> renderUser;
Note: This was how I originally believed handlers should be composed, but working with the code have pushed this operator to be far less important.
As the web server is just a composition of handlers, and a composition of handlers is in itself just a handler, we just need to serve a handler.
The function createServer
takes a middleware and returns an
http.requestListener
function, that works directly with the native http
module.
let middleware = path("path") >=> sendText("Hello from /path");
let server = createServer(middleware);
%raw
{|
// Raw JS code to explicitly show how this interacts with node's HTTP module.
const http = require('http');
const httpServer = http.createServer(server);
httpServer.listen(4000);
|};
This library is slightly inspired by Suave, but with some important changes.
Suave allows the library to store data in a Context
object. As you retrieve
data, you need to dynamically cast to the expected type, and the .net runtime
would verify type compatibility (throwing an exception if it doesn't )
This solution is not idiomatic in a strongly typed functional programming language, where you would like the type checker to catch the bug at compile time instead.
But it gets even worse with ReasonML/OCaml. The compiler uses the type system for type checking, but does not add run-time type information to the program.
Because of that, the system cannot verify the type at runtime, so a type mismatch would lead to some very, very, very nasty bugs down the line.