package naboris
Install
Dune Dependency
Authors
Maintainers
Sources
md5=f880806d42c05278f17c286b7e14c0b9
sha512=0e75a342b785eef92b6d099aa90a886467ad64fa24371b34ce4b430499c9afe80457cb59a5d4eed0300db789c16d5c96ff1e9d33b95a2a1698eb477ff3cb8490
Description
Simple http server built on httpaf and lwt
Published: 16 Feb 2020
README
Naboris
Simple, fast, minimalist web framework for OCaml/ReasonML built on httpaf and lwt.
// ReasonML
let serverConfig: Naboris.ServerConfig.t(unit) = Naboris.ServerConfig.create()
|> Naboris.ServerConfig.setRequestHandler((route, req, res) => switch(Naboris.Route.path(route)) {
| ["hello"] =>
res
|> Naboris.Res.status(200)
|> Naboris.Res.text(req, "Hello world!");
| _ =>
res
|> Naboris.Res.status(404)
|> Naboris.Res.text(req, "Resource not found.");
});
Lwt_main.run(Naboris.listenAndWaitForever(3000, serverConfig));
/* In a browser navigate to http://localhost:3000/hello */
(* OCaml *)
let server_config: unit Naboris.ServerConfig.t = Naboris.ServerConfig.create ()
|> Naboris.ServerConfig.setRequestHandler(fun route req res ->
match (Naboris.Route.path route) with
| ["hello"] ->
res
|> Naboris.Res.text req "Hello world!";
| _ ->
res
|> Naboris.Res.status 404
|> Naboris.Res.text req "Resource not found.";
) in
let _ = Lwt_main.run(Naboris.listenAndWaitForever 3000 server_config)
(* In a browser navigate to http://localhost:3000/hello *)
Contents
@@@@@ @@@@ @@@@@
*@* @@@@@@ @@&
@@& .@@@@ @@@/ @@, (@@@
, @@ @@@@@@@ @@@@@@@
@@@@@@@@, @@@@@@@@@
@@@* @@@@@@@@@@@@@@@@@@@@@@
&@@@@ @@@@@@@@@@@@@. &@@
@@@@@@@@ @@@@@@@@@@@@@@#(%(
@@@@@ @@ .@@@@@@@@@@@@@@@@@@@@@*
,@# @* @@@@@@@@@@@@@@@@@@@@@@@@@
# ,@@ @@ ,@@@@@@@@@@@@@@@@@@@@@@@@@@
.@@@@@. .@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@. @@@@ %@@@@@@@@@@@@@@@@@@@@@@@@@@@&
&@@* .@@@.@@@@@@@@@@@* (@@@@@@@@@@@@@@
@@@@@@@ @ @@@@@@@@@@ %@& @@@@@@@@@@@*
@@ @@@@@ .@@@@@@@@ @ /@@@@@@ #@@@@@
@@@@@@ @@@@@@@@@ @@@@@@@@ @
@@@ %@ @@ , .@@ %@@(@&,@@%
, @@@@* @@@@@
@@@@@ @@@@
@@@@@ @@@.
@@@@@ @@@%
@@@@@ @@@
%@ @.
(@ @
.% ,
@@( @@
Getting Started
Installation
Note
Naboris makes heavy use of Lwt. For better performance it is highly recommended (however optional) to also install conf-libev
which will configure Lwt to run with the libev scheduler. If you are using esy you will have to install conf-libev
using a special package.
conf-libev
also requires that the libev be installed. This can usually be done via your package manager.
brew install libev
or
apt install libev-dev
opam
opam install naboris
esy
"@opam/naboris": "^0.1.1"
dune
(libraries naboris)
Server Config
The Naboris.ServerConfig.t('sessionData)
type will define the way your server will handle requests.
Creating a Server Config
There are a number of helper functions for building server config records.
ServerConfig.create
create is used to generate a default server config object, this will be the starting point.
// ReasonML
let create: unit => ServerConfig.t('sessionData);
(* OCaml *)
val create: unit -> 'sessionData ServerConfig.t
ServerConfig.setOnListen
setOnListen will set the function that will be called once the server has started and is listening for connections. The onListen
function has the type signature unit => unit
.
// ReasonML
let setOnListen: (unit => unit, ServerConfig.t('sessionData)) => ServerConfig.t('sessionData)
(* OCaml *)
val setOnListen: (unit -> unit) -> 'sessionData ServerConfig.t -> 'sessionData ServerConfig.t
ServerConfig.setRequestHandler
setRequestHandler will set the main request handler function on the config. This function is the main entry point for http requests and usually where routing the request happens. The requestHandler
function has the type signature (Route.t, Req.t('sessionData), Res.t) => Lwt.t(Res.t)
.
// ReasonML
let setRequestHandler: (
(Route.t, Req.t('sessionData), Res.t) => Lwt.t(Res.t),
ServerConfig.t('sessionData)
) => ServerConfig.t('sessionData)
(* OCaml *)
val setRequestHandler: (Route.t -> 'sessionData Req.t -> Res.t -> Res.t Lwt.t)
-> 'sessionData ServerConfig.t -> 'sessionData ServerConfig.t
Routing
Routin is intended to be done via pattern matching in the main requestHandler
function. This function takes as it's first argument a Route.t
record. The Route
module looks like this:
// ReasonML
/* module Naboris.Route */
module type Route = {
type t;
let path: t => list(string),
let meth: t => Method.t,
let rawQuery: t => string,
let query: t =>Query.QueryMap.t(list(string)),
};
(* OCaml *)
module Route : sig
type t;
val path = t -> string list
val meth = t -> Method.t
val rawQuery = t -> string
val query = t -> string list Qyery.QueryMap.t
end
For these examples we'll be matching on path
and meth
.
// ReasonML
let requestHandler = (route, req, res) => switch (Naboris.Route.meth(route), Naboris.Route.path(route)) {
| (Naboris.Method.GET, ["user", userId, "contacts"]) =>
/* Use pattern matching to pull parameters out of the url */
let contacts = getContactsByUserId(userId);
let contactsJsonString = serializeToJson(contacts);
res
|> Naboris.Res.status(200)
|> Naboris.Res.json(req, contactsJsonString);
| (Naboris.Method.PUT, ["user", userId, "contacts"]) =>
/* for the sake of this example we're not using ppx or infix */
/* lwt promises can be made much easier to read by using these */
Lwt.bind(
Naboris.Req.getBody(req),
bodyStr => {
let newContacts = parseJsonString(bodyStr);
let _ = addNewContactsToUser(userId, newContacts);
res
|> Naboris.Res.status(201)
|> Naboris.Res.text(req, "Created");
},
)
| _ =>
res
|> Naboris.Res.status(404)
|> Naboris.Res.text(req, "Resource not found.");
};
(* OCaml *)
let request_handler route req res =
match ((Naboris.Route.meth route), (Naboris.Route.path route)) with
| (Naboris.Method.GET, ["user"; user_id; "contacts"]) ->
(* Use pattern matching to pull parameters out of the url *)
let contatcs = get_contacts_by_user_id user_id in
let contacts_json_string = serialize_to_json contacts in
res
|> Naboris.Res.status 200
|> Naboris.Res.json req contacts_json_string;
| (Naboris.Method.PUT, ["user"; user_id; "contacts"]) ->
(* for the sake of this example we're not using ppx or infix *)
(* lwt promises can be made much easier to read by using these *)
Lwt.bind
(Naboris.Req.getBody req)
(fun body_str ->
let new_contacts = parse_json_string body_str in
let _ = add_new_contacts_to_user user_id new_contacts in
res
|> Naboris.Res.status 201
|> Naboris.Res.text req "Created"
)
| _ ->
res
|> Naboris.Res.status 404
|> Naboris.Res.text req "Resource not found."
Static Files
Static middleware
ServerConfig.addStaticMiddleware
makes it easy to add a virtual path prefix for static assets during server configuration.
// ReasonML
let addStaticMiddleware : (list(string), string, ServerConfig.t('sessionData)) => ServerConfig.t('sessionData)
(* OCaml *)
val addStaticMiddleware : string list -> string -> 'sessionData ServerConfig.t -> 'sessionData ServerConfig.t
string list
: Split path that will match against incoming requestsstring
: Root directory from which to read static files'sessionData ServerConfig.t
: Naboris server configurationReturns
'sessionData ServerConfig.t
: New configuration with the static middleware
// ReasonML
let serverConfig = Naboris.ServerConfig.create()
|> Naboris.ServerConfig.addStaticMiddleware(["static"], Sys.getenv("cur__root") ++ "/public/");
(* OCaml *)
let server_conf = Naboris.ServerConfig.create()
|> Naboris.ServerConfig.addStaticMiddleware ["static"] ((Sys.getenv "cur__root") ^ "/static-assets/")
In the case above /static/images/icon.png
would be served from $cur__root/public/images/icon.png
Static response
Res.static
is available to help make it easy to serve static files.
// ReasonML
let static : (string, list(string), Req.t('sessionData), Res.t) => Lwt.t(Res.t)
(* OCaml *)
val static : string -> string list -> 'sessionData Req.t -> Res.t -> Res.t Lwt.t
string
: Being the root directory from which to read static filesstring list
: Being the split path from the root directory to read the specific static file'sessionData Req.t
: The current naboris requestRes.t
: The current naobirs response
A pattern matcher for static file routes might look like this
// ReasonML
switch (Naboris.Route.meth(route), Naboris.Route.path(route)) {
| (Naboris.Method.GET, ["static", ...staticPath]) =>
let publicDir = Sys.getenv("cur__root") ++ "/public/";
Naboris.Res.static(publicDir, staticPath, req, res);
}
(* OCaml *)
match ((Naboris.Route.meth route), (Naboris.Route.path route)) with
| (Naboris.Method.GET, "static" :: static_path) ->
let public_dir = (Sys.getenv "cur__root") ^ "/static-assets/") in
Naboris.Res.static public_dir static_path req res
In the case above /static/images/icon.png
would be served from $cur__root/public/images/icon.png
Session Data
Many Naboris
types take the parameter 'sessionData
this represents a custom data type that will define session data that will be attached to an incoming request.
sessionConfig
Naboris.ServerConfig.setSessionConfig will return a new server configuration with the desired session configuration. This call consists of one required argument mapSession
and two optional arguments ~maxAge
and ~sidKey
.
let setSessionConfig: (~maxAge: int=?, ~sidKey: string=?, option(string) => Lwt.t(option(Session.t('sessionData))), ServerConfig.t('sessionData)) => ServerConfig.t('sessionData);
val setSessionConfig: ?maxAge: int -> ?sidKey: string -> string option -> 'sessionData Session.t option Lwt.t -> 'sessionData ServerConfig.t -> 'sessionData ServerConfig.t
mapSession
A special function that is used to set session data on an incoming reuquest based on the requests cookies. The signature looks like: option(string) => Lwt.t(option(Naboris.Session.t('sessionData)))
. That's a complicated type signature that expresses that the request may or may not have a sessionId
; and given that fact it may or may not return a session.
// ReasonML
// Your custom data type
type userData = {
userId: int,
username: string,
firstName: string,
lastName: string,
isAdmin: bool,
};
let serverConfig: Naboris.ServerConfig(userData) = Naboris.ServerConfig.create()
|> Naboris.ServerConfig.setSessionConfig(sessionId => switch(sessionId) {
| Some(id) =>
/* for the sake of this example we're not using ppx or infix */
/* lwt promises can be made much easier to read by using these */
Lwt.bind(getUserDataById(id),
userData => {
let session = Naboris.Session.create(id, userData);
Lwt.return(Some(session));
}
);
| None => Lwt.return(None);
})
|> Naboris.ServerConfig.setRequestHandler((route, req, res) => switch(Naboris.Route.meth(meth), Naboris.Route.path(route)) {
| (Naboris.Method.POST, ["login"]) =>
let (req2, res2, _sessionId) =
/* Begin a session */
Naboris.SessionManager.startSession(
req,
res,
{
userId: 1,
username: "foo",
firstName: "foo",
lastName: "bar",
isAdmin: false,
},
);
Naboris.Res.status(200, res2) |> Naboris.Res.text(req2, "OK");
| (Naboris.Method.GET, ["who-am-i"]) =>
/* Get session data from the request */
switch (Naboris.Req.getSessionData(req)) {
| None =>
Naboris.Res.status(404, res) |> Naboris.Res.text(req, "Not found")
| Some(userData) =>
Naboris.Res.status(200, res)
|> Naboris.Res.text(req, userData.username)
};
});
(* OCaml *)
(* Your custom session data *)
type user_data = {
userId: int;
username: string;
first_name: string;
last_name: string;
is_admin: bool
}
let serverConfig: user_data Naboris.ServerConfiguserData = Naboris.ServerConfig.create ()
|> Naboris.ServerConfig.setSessionConfig (fun session_id ->
match (session_id) with
| Some(id) =>
(* for the sake of this example we're not using ppx or infix *)
(* lwt promises can be made much easier to read by using these *)
Lwt.bind (get_user_data_by_id id) (fun user_data ->
let session = Naboris.Session.create id user_data in
Lwt.return Some(session)
)
| None => Lwt.return None)
|> Naboris.ServerConfig.setRequestHandler (fun route, req, res ->
match ((Naboris.Route.meth route), (Naboris.Route.path route)) with
| (Naboris.Method.POST, ["login"]) ->
let (req2, res2, _session_id) =
(* Begin a session *)
Naboris.SessionManager.startSession req res {
userId= 1;
username= "foo";
first_name= "foo";
last_name= "bar";
is_admin= false
} in
Naboris.Res.status 200 res2
|> Naboris.Res.text req2, "OK"
| (Naboris.Method.GET, ["who-am-i"]) ->
(* Get session data from the request *)
match (Naboris.Req.getSessionData req) with
| None ->
Naboris.Res.status 404 res
|> Naboris.Res.text req "Not found"
| Some(user_data) ->
Naboris.Res.status 200 res
|> Naboris.Res.text req user_data.username)
sidKey and maxAge
sidKey
-string
(optional) - The key used to store the session id in browser cookies. Defaults to"nab.sid"
.maxAge
-int
(optional) - The max age of session cookies in seconds. Defaults to2592000
(30 days.)
SessionManager.startSession
Generates a new session id string
value and adds Set-Cookie
header to a new Res.t
. Useful for handling a login request.
let startSession: (Req.t('sessionData), Res.t, 'sessionData) => (Req.t('sessionData), Res.t, string);
val startSession: 'sessionData Req.t -> Res.t -> 'sessionData -> 'sessionData Req.t * Res.t * string
An example login request might look like this:
| (Naboris.Method.POST, ["login"]) =>
let (req2, res2, _sid) =
Naboris.SessionManager.startSession(
req,
res,
TestSession.{username: "realsessionuser"},
);
Naboris.Res.status(200, res2) |> Naboris.Res.text(req2, "OK");
| (Naboris.Method.POST, ["login"]) ->
let (req2, res2, _sid) = Naboris.SessionManager.startSession
req
res
TestSession.{username= "realsessionuser"} in
(Naboris.Res.status 200 res2) |> Naboris.Res.text req2 "OK"
SessionManager.removeSession
Adds Set-Cookie
header to a new Res.t
to expire the session id cookie. Useful for handling a logout request.
let removeSession: (Req.t('sessionData), Res.t) => Res.t;
val removeSession: 'sessionData Req.t -> Res.t -> Res.t
An example logout request might look like this:
| (Naboris.Method.GET, ["logout"]) =>
Naboris.SessionManager.removeSession(req, res)
|> Naboris.Res.status(200)
|> Naboris.Res.text(req, "OK");
| (Naboris.Method.GET, ["logout"]) ->
Naboris.SessionManager.removeSession req res
|> Naboris.Res.status 200
|> Naboris.Res.text req "OK";
Advanced
Middlewares
Middlewares have a wide variety of uses. They are executed in the order in which they are registered so be sure to keep that in mind. Middlewares are functions with the following signature:
Naboris.RequestHandler.t -> Naboris.Route.t -> Naboris.Req.t -> Naboris.Res.t -> Res.t Lwt.t
Middlewares can either handle the http request/repsonse lifecycle themselves or call the passed in request handler, which is the next middleware in the list, passing the route, req, and res. Once the list of middlewares has been exaused it will then be passed on to the main request handler.
One simple example of a middleware would be one that protects certain routes from users without specific permissions.
Given the Sesson Data example above, one such middleware might look like this:
// ReasonML
let serverConf: Naboris.ServerConfig.t(userData) = Naboris.ServerConfig.create()
|> Naboris.ServerConfig.addMiddleware((next, route, req, res) => switch (Naboris.Route.path(route)) {
| ["admin", ..._] => switch (Naboris.Req.getSessionData(req)) {
| Some({ is_admin: true, ..._}) => next(route, req, res)
| _ =>
res
|> Naboris.Res.status(401)
|> Naboris.Res.text(req, "Unauthorized");
}
| _ => next(route, req, res)
});
(* OCaml *)
let server_conf: user_data Naboris.ServerConfig.t = Naboris.ServerConfig.create ()
|> Naboris.ServerConfig.addMiddleware (fun next route req res ->
match (Naboris.Route.path route) with
| "admin" :: _ ->
(match (Naboris.Req.getSessionData req) with
| Some({ is_admin = true; _}) -> next route req res
| _ ->
res
|> Naboris.Res.status 401
|> Naboris.Res.text req "Unauthorized")
| _ -> next route req res)
RequestHandler.t
also return Lwt.t(Res.t)
and this can be used to inspect the response record after the request has been served. This could be useful for logging as an example:
// ResonML
let serverConfig = Naboris.ServerConfig.addMiddleware((next, route, req, res) => {
Lwt.bind(() => next(route, req, res), (servedResponse) => {
print_endline("Server responded with status " ++ int_of_string(Res.status(servedResponse)));
});
}, oldServerConfig);
(* OCaml *)
let serverConfig = Naboris.ServerConfig.addMiddleware (fun (next, route, req, res) ->
Lwt.bind
(fun () -> next route req res)
(fun (served_res) ->
print_endline "Server responded with status " ^ (int_of_string (Res.status served_res))
)
)
oldServerConfig in
Development
Any help would be greatly appreciated! 👍
To run tests
esy install
npm run test
Breaking Changes
From | To | Breaking Change |
---|---|---|
0.1.0 |
0.1.1 |
ServerConfig.setSessionGetter changed to ServerConfig.setSessionConfig which also allows ~maxAge and ~sidKey to be passed in optionally. |
0.1.0 |
0.1.1 |
All RequestHandler.t and Middleware.t now return Lwt.t(Res.t) instead of Lwt.t(unit) |
0.1.0 |
0.1.1 |
Res.reportError now taxes exn as the first argument to match more closely the rest of the Res API. |