View Source nine
A library providing a data driven router compiler.
The goal of nine is to allow developers to compose middleware and handlers.
This should significantly decrease boiler plate and give a web development experience competitive with other
languages and ecosystems.
Build
rebar3 compileUsage
nine functions as a router compiler library. Meaning you give it a router config and it compiles a router module.
nine does not have it's own web server, it is meant to be paired with a backend.
nine provides one single function to be used: compile. Compile can be given one map argument or 3 arguments.
nine:compile(#{routes => Routes, router => Router, generator => Generator})OR
nine:compile(Routes, Router, Generator).The generator is provided by the backend, and is a module required to have at least one function generate/2.
The name of the Router module and the normalized routes after nine compiles the routes config given.
nine:compile takes a router config and compiles it into an Erlang module at runtime using forms.
Routes
A route is a map with these keys:
[{<<"path">> => ...,
<<"method">> => ...,
<<"pre">> => ...,
<<"post">> => ...,
<<"handle">> => ...}]path: The URL path for the request to be handled. Example: <<"/todo">>. method: The request method to be handled. Example: <<"GET">>. pre: The middleware to be called before the request handler. Example: [{todo_mid, json_request}] post: The middleware to be called after the request handler. Example: [{todo_mid, json_response}] handle: The request handler function.
A minimum config might look like this:
[#{<<"path">> => <<"/">>,
<<"handle">> => {index_handler, index}}]A full config for a todo application would look like this:
[#{<<"path">> => <<"/">> =>
<<"method">> => <<"GET">>,
<<"handle">> => {todo_handler, index}},
#{<<"path">> => <<"/api">>,
<<"post">> => [{todo_mid, json_response}],
<<"handle">> => [
#{<<"path">> => <<"/todo">>,
<<"pre">> => [{todo_mid, json_request}],
<<"handle">> => [
#{<<"method">> => <<"POST">>,
<<"handle">> => {todo_handler, post_todo}},
#{<<"method">> => <<"DELETE">>,
<<"handle">> => {todo_handler, delete_todo}}
],
#{<<"path">> => <<"/todos">>,
<<"method">> => <<"GET">>,
<<"handle">> => {todo_handler, get_todos}}
]
},
#{<<"path">> => <<"*">>,
<<"handle">> => {todo_handler, not_found}}
]There is a lot going on here with this config which we will break down in the next sections.
Handler
A handler is specified as {module, function}. Example:
{todo_handler, get_todos}todo_handler being the module, and get_todos is the function.
The handler function is expected to look like this:
get_todos(Context) ->
Context#{resp => thoas:encode([#{id => 1, body => <<"Write more Erlang">>}])}.Nested Handlers
It is possible to compose complex router configs by nesting handlers. The handle key can either take a
tuple or a list of route configs.
For example an api handler can branch off with multiple other request paths.
[{<<"path">> => <<"/api">>,
<<"handle">> => [
#{<<"path">> => <<"todo">>,
<<"method">> => <<"POST">>,
<<"handle">> => {todo_handler, post_todo}},
#{<<"path">> => <<"todos">>,
<<"method">> => <<"GET">>,
<<"handle">> => {todo_handler, get_todos}}
]}]This means when a POST request goes to /api/todo the function todo_handler:post_todo/1 will be called. While
a request going to /api/todos will trigger the function todo_handler:get_todos/1.
URL Path Params
nine builds in a way to have named parameters in the URL.
A path like /todo/:id will result in the context map including the params key.
The value of the params will be #{id => <<"id1">>}.
In case you are worried about atoms coming from user data, it is okay for id to be an atom because it is a static value set at compile time.
nine also supports partial path params like /person/num:ber will result in a params map looking like #{ber => <<"2">>} for example. This is similar to how Phoenix works with routing.
Wildcard
nine supports catch all routes with * in the path. For example: <<"/*">> will match any route.
We can also put a wildcard at the suffix of a path: <<"/foo/*">> will match a route like <<"/foo/bar">>.
Wildcards can only be at the end of a path. This is similar to how Phoenix works with catch all routes.
The reason for this is the routing uses Erlang pattern matching, so it must follow the same rules.
Middleware
Middleware are specified just like handlers, in fact they are the same thing! An example middleware might look like:
{nine_mid, json_response}Middleware are functions that take a Context as input and output a Context or an elli response. One could write a logging middleware like this:
logging_middleware(Context) ->
logger:debug(#{context => Context}),
Context.Or we could make a middleware that adds some data to the Context:
message_middleware(Context) ->
Context#{message => <<"Hello, World!">>}.Middleware are helpful in all sorts of situations and allow developers to write web apps in a DRY way.
Middleware Chains
nine specifies middleware chaining with the pre and post keys. Nesting routes will also concatenate the pre and post keys
in the expected order.
For example:
#{<<"pre">> => [{todo_mid, json_request}],
<<"handle">> => {todo_handler, post_todo}}Will generate a sequence of function calls where todo_mid:json_request is called first, and the result is passed to
todo_handler:post_todo.
Allowing post_todo to be implemented like so:
post_todo(Context=#{json := #{<<"body">> := Body}}) ->
todo_db:insert(Body),
nine_util:redirect(Context, <<"/">>).post_todo can expect the json key to be filled with data because json_request is called first.
Halting
There are situations where we want to return a response immediately without finishing the middleware chain. This is known as halting.
nine makes this possible because each middleware and handler call is wrapped in a case statement checking for the resp key.
If a handler or middleware returns a Context map with the resp key it will immediately be sent without triggering further middleware.
Inspirations
nine was inspired by other composable middleware tools.
- ring - Standard Clojure HTTP abstraction for web servers
- ataraxy - data driven routing library for Clojure
- Plug.Router - Ecosystem defining Elixir HTTP middleware
- golang http middleware - Standard Library Golang Middleware Pattern
- Cowboy Router - cowboy router is compiled into a lookup table
Fun Facts
- The name
ninecomes from "nine nines". - Middleware was originally intended to look like Ring's, but wasn't compatible with Erlang's pattern matching lookups.