purescript-hyper / hyper

Type-safe, statically checked composition of HTTP servers
https://hyper.wickstrom.tech
Mozilla Public License 2.0
282 stars 29 forks source link

Adapter for express js middleware #47

Open Risto-Stevcev opened 7 years ago

Risto-Stevcev commented 7 years ago

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?

owickstrom commented 7 years ago

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.

Risto-Stevcev commented 7 years ago

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

srghma commented 4 years ago

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'