Thanks everyone for the comments.
In general it seems like the general consensus it that an important quality to factor in here is familiarity:
The framework needs to feel safe and familiar to users coming from other frameworks, but differences can be tolerated if they can be justified and are adequately explained.
Increased typesafety should also be important to the design, but should not necessarily come at the cost of reduced usability. This may require some form of compiler extension.
I still feel that suave.io is still an admirable model to crib from, but I believe I might have found a general design which could compose as well as suave while still supporting other “flavours” of building APIs. The main difference is that express middleware has the general signature of (request, response) => unit
.
Changing this to:
module HttpContext = {
type t = { request, response };
};
type result =
| Handled(HttpContext.t)
| Unhandled;
type t = (HttpContext.t) => Promise.t(result);
Allows you to perform monadic operations on the ctx object, forces middleware to return a result (middleware also now becomes just function composition) and could support a familiar API to that of express.
Adding support for different compilation targets would be a matter of mapping from the request record and mapping to the response record, along with perhaps offering cross-platform niceties such as an isomorphic JSON encoding/decoding api.