hyper
hyper copied to clipboard
Adapter for express js middleware
Being able to reuse existing express middleware would be really nice in hyper. As it currently is, nodejs apps using express that are transitioning to purescript wouldn't be able to switch to hyper immediately because all of their infrastructure would be bound to the express middleware. They would be forced to either use the express wrapper (not ideal) or leave it as is (even worse).
I think hyper middleware should be completely compatible with express middleware as far as I can tell. What needs to be done to get this adapter going?
This is indeed an interesting proposal, and it has been on my TODO list before. :slightly_smiling_face: Not sure why there's no issue or note of it anywhere.
As long as they're using the Hyper.Node.Server, then yes, everything could be compatible. Then it is up to the one doing the FFI binding to provide a proper type signature, and perhaps to provide a value in the components
of the Conn. Such a value can be parameterized to safely track state, if needed.
The tricky part, I guess, is that many Express middleware might perform a side effect, or not, and then just hand over to the next, which also might perform side effects. Indeed, this is why I started with Hyper in the first place. :smile: But I think Express middleware should be "lifted" to Hyper on an individual basis, not with a general liftMiddleware
that "works" on any Express middleware, as their type signatures and effects probably will differ a lot.
One other thing, which I wanted to write about but I haven't had time, is how error handling differs with Hyper from Express (as we're tracking effects). In Express, errors are passed in continuations to signal that the response hasn't been sent and that it's up to someone else to do it. In Hyper, you need to "wrap" fallback middleware with other middleware that might fail. For example, the fileServer middleware takes an on404
middleware as a fallback. This affects how Express middleware should be integrated as well. Maybe a generic function for doing FFI, that takes an Express middleware and returns a Hyper middleware with a similar type to fileServer
, could work.
To sum up; I think this is valuable step for Hyper to take, and that it could, as you say, enable more projects to gradually migrate from Express. That is probably the selling point. I think Haskellers are unlikely to jump on the NodeJS runtime just to track side effects in middleware with Hyper, but battle-scarred NodeJS developers might long for some type safety in their backend projects.
Yeah that's true. For the express middleware I've used before, it modifies the request or response object and add a new property to it, and it also performs a side effect like reading/writing to a database (like for a store for passportjs) I might try to write some kind of adapter, I'm transitioning an old nodejs express app and I really want to use hyper for it instead of express. But I'm a ways away from getting to that part of the code
here is an example
module Server where
import Prelude
import Effect
import Effect.Aff
import Effect.Aff.Class (liftAff, class MonadAff)
import Data.Tuple (Tuple(Tuple))
import Control.Monad.Indexed.Qualified as IndexedMonad
import Hyper.Node.FileServer as Hyper
import Hyper.Node.Server as Hyper
import Hyper.Response as Hyper
import Hyper.Request as Hyper
import Hyper.Status as Hyper
import Hyper.Middleware as Hyper
import Hyper.Conn as Hyper
import Node.Encoding as NodeBuffer
import Node.Buffer as NodeBuffer
import Data.Function.Uncurried as Functions
import Effect.Uncurried as Effect.Uncurried
import Node.Path as Path
import Node.HTTP as NodeHttp
import Data.Newtype as Newtype
import Debug.Trace as Debug
type ForeignMiddleware = Functions.Fn3 NodeHttp.Request NodeHttp.Response (Effect Unit) (Effect Unit)
mkMiddlewareFromForeign
:: forall c
. ForeignMiddleware
-> Hyper.Middleware
Aff
(Hyper.Conn Hyper.HttpRequest (Hyper.HttpResponse Hyper.StatusLineOpen) c)
(Hyper.Conn Hyper.HttpRequest (Hyper.HttpResponse Hyper.ResponseEnded) c)
Unit
-> Hyper.Middleware
Aff
(Hyper.Conn Hyper.HttpRequest (Hyper.HttpResponse Hyper.StatusLineOpen) c)
(Hyper.Conn Hyper.HttpRequest (Hyper.HttpResponse Hyper.ResponseEnded) c)
Unit
mkMiddlewareFromForeign foreignMiddleware (Hyper.Middleware app) = Hyper.Middleware $ \conn ->
makeAff \cb -> do
let (Hyper.HttpRequest (nodeHttpRequest :: NodeHttp.Request) _requestData) = conn.request
(Hyper.HttpResponse nodeHttpResponse) = conn.response
onNext = do
Debug.traceM "traceM: before calling cb"
runAff_ cb (app conn)
Debug.traceM "traceM: after calling cb"
Debug.traceM "traceM: before calling foreignMiddleware"
Functions.runFn3 foreignMiddleware nodeHttpRequest nodeHttpResponse onNext
Debug.traceM "traceM: after calling foreignMiddleware"
pure nonCanceler
foreign import _testMiddleware :: ForeignMiddleware
testMiddleware
:: forall c
. Hyper.Middleware
Aff
(Hyper.Conn Hyper.HttpRequest (Hyper.HttpResponse Hyper.StatusLineOpen) c)
(Hyper.Conn Hyper.HttpRequest (Hyper.HttpResponse Hyper.ResponseEnded) c)
Unit
-> Hyper.Middleware
Aff
(Hyper.Conn Hyper.HttpRequest (Hyper.HttpResponse Hyper.StatusLineOpen) c)
(Hyper.Conn Hyper.HttpRequest (Hyper.HttpResponse Hyper.ResponseEnded) c)
Unit
testMiddleware = mkMiddlewareFromForeign _testMiddleware
main :: Effect Unit
main =
let
app = IndexedMonad.do
Debug.traceM "traceM: app is called"
Hyper.writeStatus Hyper.statusOK
Hyper.headers []
Hyper.respond "<h1>Hello from app!</h1>"
app' = app # testMiddleware
in Hyper.runServer Hyper.defaultOptionsWithLogging {} app'
const testMiddlewareEndsResponse = function(req, res, nxt) {
console.log('testMiddlewareEndsResponse')
console.log('Outside')
console.log('Request Type:', req.method)
return function() {
console.log('Inside')
console.log('Request Type:', req.method)
console.log('Ending reponse')
res.writeHead(200);
res.end('<h1>Hello from testMiddlewareEndsResponse!</h1>')
}
}
const testMiddlewareCallsNext = function(req, res, next) {
console.log('testMiddlewareCallsNext')
console.log('Outside')
console.log('Request Type:', req.method)
return function() {
console.log('Inside')
console.log('Request Type:', req.method)
console.log('calling next')
next()
}
}
exports._testMiddleware = testMiddlewareCallsNext
examle1 output
Listening on http://0.0.0.0:3000
'traceM: before calling foreignMiddleware'
testMiddlewareEndsResponse
Outside
Request Type: GET
Inside
Request Type: GET
Ending reponse
'traceM: after calling foreignMiddleware'
examle2 output
Listening on http://0.0.0.0:3000
'traceM: before calling foreignMiddleware'
testMiddlewareCallsNext
Outside
Request Type: GET
Inside
Request Type: GET
calling next
'traceM: before calling cb'
'traceM: app is called'
'traceM: after calling cb'
'traceM: after calling foreignMiddleware'