10 KiB
Writing and Using Middleware in HTTPurple 🪁
Since HTTPurple 🪁 routers are just pure functions, you can write a middleware by
simply creating a function that takes a router and an HTTPure.Request
, and
returns an HTTPure.ResponseM
. You can then simply use function composition to
combine middlewares, and pass your router to your composed middleware to
generate the decorated router!
See the Middleware example to see how you can build, compose, and consume different types of middleware.
🎉 New:
HTTPurple 🪁 now supports extensible middlewares that allows you to add further data to your request, see Extensible Middlewares.
HTTPurple 🪁 now supports Node.js/Express middlewares, see Node.js Middlewares.
Writing Middleware
A middleware is a function with the signature:
(HTTPure.Request -> HTTPure.ResponseM) -> HTTPure.Request -> HTTPure.ResponseM
Note that the first argument is just the signature for a router function. So essentially, your middleware should take a router and return a new router. That's it! You can do pretty much anything with middlewares. Here are a few examples of common middleware patterns:
You can write a middleware that wraps all future work in some behavior, like logging or timing:
myMiddleware router request = do
doSomethingBefore
response <- router request
doSomethingAfter
pure response
Or perhaps a middleware that injects something into the response:
myMiddleware router request = do
response <- router request
HTTPure.response' response.status response.headers $
response.body <> "\n\nGenerated using my super duper middleware!"
You could even write a middleware that handles routing for some specific cases:
myMiddleware _ { path: [ "somepath" ] } = HTTPure.ok "Handled by my middleware!"
myMiddleware router request = router request
Or even a middleware that conditionally includes another middleware:
myMiddleware router = if something then someOtherMiddleware router else router
Just make sure your middlewares follow the correct signature, and users will be able to compose them at will!
Note that because there is nothing fancy happening here, you could always write higher order functions that don't follow this signature, if it makes sense. For instance, you could write a function that takes two routers, and selects which one to use based on some criteria. There is nothing wrong with this, but you should try to use the middleware signature mentioned above as much as possible as it will make your middleware easier to consume and compose.
Consuming Middleware
Consuming middleware easy: simply compose all the middleware you want, and then pass your router to the composed middleware. For instance:
main = HTTPure.serve port composedRouter $ Console.log "Server is up!"
where
composedRouter = middlewareA <<< middlewareB <<< middlewareC $ router
Be aware of the ordering of the middleware that you compose--since we used
<<<
, the middlewares will compose right-to-left. But because middlewares
choose when to apply the router to the request, this is a bit like wrapping the
router in each successive middleware from right to left. So when the router
executes on a request, those middlewares will actually execute
left-to-right--or from the outermost wrapper inwards.
In other words, say you have the following HTTPurple 🪁 server:
middleware letter router request = do
EffectClass.liftEffect $ Console.log $ "Starting Middleware " <> letter
response <- router request
EffectClass.liftEffect $ Console.log $ "Ending Middleware " <> letter
pure response
main = HTTPure.serve port composedRouter $ Console.log "Server is up!"
where
composedRouter = middleware "A" <<< middleware "B" $ router
When this HTTPurple 🪁 server receives a request, the logs will include:
Starting Middleware A
Starting Middleware B
...
Ending Middleware B
Ending Middleware A
Extensible Middlewares
The base type for requests, ExtRequest
is now extensible:
type RequestR route ext =
( method :: Method
, path :: Path
, query :: Query
, route :: route
, headers :: RequestHeaders
, body :: RequestBody
, httpVersion :: Version
, url :: String
| ext
)
type ExtRequest route ext = { | RequestR route ext }
and the old Request
is just a type alias for an extensible request without any further data:
type Request route = { | RequestR route () }
This allows us to write middlewares that extend our request with additional information.
E.g. we can write an authenticator middleware that adds user information to the request:
authenticator ::
forall route extIn extOut.
Nub (RequestR route extOut) (RequestR route extOut) =>
Union extIn (user :: Maybe String) extOut =>
Middleware route extIn extOut
authenticator router request@{ headers } = case Headers.lookup headers "X-Token" of
Just token | token == "123" -> router $ merge request { user: Just "John Doe" }
_ -> router $ merge request { user: Nothing :: Maybe String }
The type Middleware
is defined as
type MiddlewareM m route extIn extOut = (ExtRequest route extOut -> m Response) -> ExtRequest route extIn -> m Response
type Middleware route extIn extOut = MiddlewareM Aff route extIn extOut
and adds the extOut
extension to our request handler. In our case, it adds (user :: Maybe String)
to the request handler. extIn
defines the input extension that the middleware receives. At the root level, this will be an empty row ()
(unless we use a node middleware).
Not fixing extIn
to ()
however allows us to stack our middlewars.
Let's add another middleware, that adds the request time to the request:
requestTime ::
forall route extIn extOut.
Nub (RequestR route extOut) (RequestR route extOut) =>
Union extIn (time :: JSDate) extOut =>
Middleware route extIn extOut
requestTime router request = do
time <- liftEffect JSDate.now
router $ merge request { time }
Similar to the authenticator
middleware, we add a new field time
to the request.
We can now compose our middleware stack:
middlewareStack :: forall route. (ExtRequest route (user :: Maybe String, time :: JSDate) -> ResponseM) -> Request route -> ResponseM
middlewareStack = authenticator <<< requestTime
We can now use our middleware stack. Let's define a simple route:
data SayHello = SayHello
derive instance Generic SayHello _
sayHelloRoute :: RD.RouteDuplex' SayHello
sayHelloRoute = RD.root $ RG.sum
{ "SayHello": RG.noArgs
}
and a router that makes use of our newly added information:
-- | Say 'hello <USER>' when run with X-Token, otherwise 'hello anonymous'
sayHello :: ExtRequest SayHello (user :: Maybe String, time :: JSDate) -> ResponseM
sayHello { user: Just user, time } = ok $ "hello " <> user <> ", it is " <> JSDate.toDateString time <> " " <> JSDate.toTimeString time
sayHello { user: Nothing, time } = ok $ "hello " <> "anonymous, it is " <> JSDate.toDateString time <> " " <> JSDate.toTimeString time
As you can see, we are now using an ExtRequest
with the additional information (user :: Maybe String, time :: JSDate)
, which we can use in our function body.
Finally, we wrap our sayHello
router with our middlewareStack
:
main =
serve { hostname: "localhost", port: 8080 } { route: sayHelloRoute, router: middlewareStack sayHello }
See the full example in the Examples/ExtensibleMiddleware
folder.
Node Middlewares
Node/Express middlewares are no supported, but currently only on the application level (i.e. they will be run on every request).
For our example, we'll use two existing node middlewares, and one custom node middleware.
The first one is morgan
, which adds logging to our http server.
Install it using
npm install morgan --save
To use it, we create a FFI and export it as morgan
import { default as M } from "morgan";
export const morgan = M("tiny")
Now we can define the foreign import in our Purescript file:
foreign import morgan :: NodeMiddleware ()
The empty row ()
indicates, that this middleware doesn't add any information to our request.
Next, we'll add helmet
which adds some security headers to our response.
npm install helmet --save
Add it to our ffi
import { default as H } from "helmet";
export const helmet = H()
and define the foreign import
foreign import helmet :: NodeMiddleware ()
Finally, let's define a small node middleware that adds something to our request. Add a simple authenticating middleware to our ffi:
export const authenticator = function (req, res, next) {
if(req.headers["x-token"] == "123") {
req.user = "John Doe"
} else {
req.user = null
}
next();
};
If the middleware receives the http header x-token
with value 123
, it will add the user, otherwise null
.
Our foreign import now looks like this:
type AuthenticatorR = (user :: Nullable String)
foreign import authenticator :: NodeMiddleware (user :: Nullable String)
Our node middleware extends requests with (user :: Nullable String)
.
We can now compose our middlewares into a single NodeMiddlewareStack
:
nodeMiddleware ∷ NodeMiddlewareStack () AuthenticatorR
nodeMiddleware = NodeMiddlewareStack $ usingMiddleware morgan >=> usingMiddleware helmet >=> usingMiddleware authenticator
Let's define a simple route with a handler, that makes use of our node middleware:
data Route = Hello
derive instance Generic Route _
route :: RouteDuplex' Route
route = mkRoute
{ "Hello": noArgs
}
main :: ServerM
main =
serveNodeMiddleware { port: 8080 } { route, router: router, nodeMiddleware }
where
router { route: Hello, user } = case Nullable.toMaybe user of
Just u -> ok $ "hello user " <> u
Nothing -> ok $ "hello anonymous"
Note, that we have to use serveNodeMiddleware
instead of serve
and pass the nodeMiddleware
along the route
and router
. In the router, we gain access to the nullable user.
You can see a full example of node middlewares in the Examples/NodeMiddleware
folder.