8.7 KiB
HTTPurple 🪁
A 🎨 colourful fork of the amazing HTTPure http server framework.
Coming from HTTPure? You might want to have a look at the differences to HTTPure.
ToC
Installation
spago install httpurple
Quick start
module Main where
import Prelude hiding ((/))
import HTTPurple
data Route = Hello String
derive instance Generic Route _
route :: RouteDuplex' Route
route = mkRoute
{ "Hello": "hello" / segment
}
main :: ServerM
main =
serve { port: 8080 } { route, router }
where
router { route: Hello name } = ok $ "hello " <> name
then start the server
➜ spago run
Src Lib All
Warnings 0 0 0
Errors 0 0 0
[info] Build succeeded.
HTTPurple 🪁 up and running on http://0.0.0.0:8080
query your server, e.g. using httpie
➜ http http://localhost:8080/hello/🗺
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 10
Date: Sun, 22 May 2022 16:50:52 GMT
Keep-Alive: timeout=5
hello 🗺
Documentation
See the docs folder for more in-depth guides.
Examples
HTTPurple ships with a number of examples. To run an example, in the project root, run:
spago -x test.dhall run --main Examples.<Example Name>.Main
Each example's startup banner will include information on routes available on the example server.
Testing
To run the test suite, in the project root run:
spago -x test.dhall test
Differences to HTTPure
HTTPurple 🪁 is a fork of HTTPure that I started to freely experiment with some ideas I have on improving the usage experience. Currently I have no intentions on back-porting any of it to HTTPure, as I don't have the time for it and also don't want to restrict myself.
If you have used HTTPure before, you'll probably want to go through the following changes to get started using HTTPurple 🪁:
- routing-duplex
- startup options
- request parsing and validation
- other improvements
- hot module reloading
Routing-duplex
The most notable difference to HTTPure is that HTTPurple 🪁 uses the amazing routing-duplex
library for routing. I found the previous lookup-based routing tedious to work with, especially when having more complex routes, and quite error-prone, especially if you need reverse-routing for redirects.
routing-duplex
offers an elegant bidirectional routing which was initially designed for SPAs. Have a look at the really extensive documentation
. The benefits of using routing-duplex are
- Much simpler and less tedious definition of routes
- Roundtrip printing/parsing of routes, so no more invalid redirects
- Exhaustive pattern matching so you are sure to match all defined routes
- Option to separate routes into logical groups
Here is a bit more elaborated examples:
module Main where
import Prelude hiding ((/))
import Data.Either (Either(..))
import Data.Generic.Rep (class Generic)
import Data.Maybe (Maybe(..))
import Data.Tuple (Tuple(..))
import HTTPurple
data Route
= Home
| Profile String
| Account String
| Search { q :: String, sorting :: Maybe Sort }
derive instance Generic Route _
data Sort = Asc | Desc
derive instance Generic Sort _
sortToString :: Sort -> String
sortToString = case _ of
Asc -> "asc"
Desc -> "desc"
sortFromString :: String -> Either String Sort
sortFromString = case _ of
"asc" -> Right Asc
"desc" -> Right Desc
val -> Left $ "Not a sort: " <> val
sort :: RouteDuplex' String -> RouteDuplex' Sort
sort = as sortToString sortFromString
api :: RouteDuplex' Route
api = root $ sum
{ "Home": noArgs
, "Profile": "profile" / string segment
, "Account": "account" / string segment
, "Search": "search" ? { q: string, sorting: optional <<< sort }
}
main :: ServerM
main = serve { port: 8080 } { route: api, router: apiRouter }
where
apiRouter { route: Home } = ok "hello world!"
apiRouter { route: Profile profile } = ok $ "hello " <> profile <> "!"
apiRouter { route: Account account } = found' redirect ""
where
reverseRoute = print api $ Profile account
redirect = headers [ Tuple "Location" reverseRoute ]
apiRouter { route: Search { q, sorting } } = ok $ "searching for query " <> q <> " " <> case sorting of
Just Asc -> "ascending"
Just Desc -> "descending"
Nothing -> "defaulting to ascending"
Startup options
HTTPurple 🪁 greatly simplifies the startup options and functions. The serve
, serve'
, serveSecure
and serveSecure'
have been merged into a single function serve
that accepts listen options as the first parameter and uses sane defaults if you don't provide any.
The easiest way to start a server is to provide just the route and a router:
main :: ServerM
main =
serve {} { route, router }
This will spin up the http server with sane defaults.
HTTPurple 🪁 up and running on http://0.0.0.0:8080
But you can overwrite any of the optional properties like this
main :: ServerM
main =
serve {
hostname: "localhost"
, port: 9000
, certFile: "./Certificate.cer"
, keyFile: "./Key.key"
, notFoundHandler
, onStarted: log "Server started 🚀"
, closingHandler: NoClosingHandler
} { route, router }
where
notFoundHandler :: Request Unit -> ResponseM
notFoundHandler = const $ ok "Nothing to see here"
Request parsing and validation
HTTPurple 🪁 makes request parsing and validation super simple. My typical http service scenario looks like this:
- Parse the request json and return a bad request if the request body doesn't contain the valid json format
- Validate the json input semanticall and transform it into some kind of internal model. Return bad request (with some error code) in case it is invalid.
- Do something with the request
- Return the output as a json
HTTPurple 🪁 uses continuations to make this standard scenario straight-forward (see example below).
Furthermore, HTTPurple 🪁 doesn't mandate a json parsing library. So you can use argonaut
using the argonaut-driver
, use yoga-json
using the yoga-json-driver
or write your own json driver.
Here is an example how that looks like:
apiRouter { route: Home, method: Post, body } = usingCont do
req@{ name } :: HelloWorldRequest <- fromJson Argonaut.jsonDecoder body
ok $ "hello " <> name <> "!"
In case fromJson
succeeds, the next step will be executed, otherwise a 400 bad request is returned.
Other improvmenets
- Default closing handler - A default closing handler is provided so you can just stop your server using
ctrl+x
without having to worry about anything. You can deactivate it by settingclosingHandler: NoClosingHandler
in the listen options.
Hot module reloading
With HTTPurple 🪁 you can easily set up a hot module reloading workflow:
Create an index.js
with the content:
import * as Main from './output/Main/index.js'
Main.main()
Add to package.json
:
...
"scripts": {
"hot": "spago build -w & nodemon \"node index.js\""
},
"type": "module",
...
Spin up:
npm run hot
Develop:
HTTPurple 🪁 up and running on http://0.0.0.0:8080
[nodemon] restarting due to changes...
[nodemon] restarting due to changes...
[nodemon] starting `node "node index.js" index.js`
HTTPurple 🪁 up and running on http://0.0.0.0:8080
[nodemon] restarting due to changes...
[nodemon] restarting due to changes...
[nodemon] starting `node "node index.js" index.js`
HTTPurple 🪁 up and running on http://0.0.0.0:8080
License
This is a fork of HTTPure, which is licensed under MIT. See the original license. This work is similarly licensed under MIT.