Search code examples
haskellservant

Catch-all or default routing


These days it's not uncommon to need to return a file (say, index.html) from the backend if the requested route doesn't match an existing API endpoint or another static asset. This is especially handy when using react-router and browserHistory.

I'm a bit stumped as to how I might approach this with Servant. I did wonder if intercepting 404's might be the way to go, but then of course sometimes the API will need to legitimately issue 404. Here's the kind of thing I've been using to experiment:

data Wombat = Wombat
  { id :: Int
  , name :: String
  } deriving (Eq, Show, Generic)
instance ToJSON Wombat

wombatStore :: [Wombat]
wombatStore = 
  [ Wombat 0 "Gertrude"
  , Wombat 1 "Horace"
  , Wombat 2 "Maisie"
  , Wombat 3 "Julius"
  ]

wombats :: Handler [Wombat]
wombats = return wombatStore

wombat :: Int -> Handler Wombat
wombat wid = do
  case find (\w -> Main.id w == wid) wombatStore of
    Just x -> return x
    Nothing -> throwE err404

type API = 
    "api" :> "wombats" :> Get '[JSON] [Wombat] :<|>
    "api" :> "wombats" :> Capture "id" Int :> Get '[JSON] Wombat :<|>
    Raw

api :: Proxy API
api = Proxy

server :: Server API 
server = wombats
  :<|> wombat
  :<|> serveDirectory "static"

app :: Application
app = serve api server

main :: IO ()
main = run 3000 app

I'd love to see an example of how I could go about adding a 'default route' that sends an HTML response if the request doesn't match an API endpoint or anything in the static directory. Toy repo here.


Solution

  • You got it, basically. serveDirectory "static" can be replaced by any wai Application, so for instance, we could have:

    ...
    {-# LANGUAGE OverloadedStrings #-}
    ...
    import Network.Wai        (responseLBS)
    import Network.HTTP.Types (status200)
    ...
    server :: Server API 
    server = wombats
        :<|> wombat
        :<|> hello
    
    hello :: Application
    hello req respond = respond $ 
      responseLBS 
      status200                        -- 
      [("Content-Type", "text/plain")] -- headers
      "Hello, World!"                  -- content
    ...
    

    To a first approximation, wai applications are simply Request -> Response, but the docs tell a fuller story:

    Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
    

    So since you've got access to IO, you can check if the file exists and if so serve it, otherwise do whatever you like. In fact, wai defines type Middleware = Application -> Application, so you might think up a handy Middleware that wraps up hello (or any other Application!) in a file existence-checker-and-server.