WIP
HTTP server library inspired by axum
, allowing best-in-class
expressive routing.
The main difference between this server library compared to others (eg. the wonderful httpurple
)
is the philosophy around routing.
The core abstraction is a Handler
; any function with the type [...Request parts] -> m Response
.
This allows each REST action to correspond to a single function, which declares its requirements in its type signature.
This allows for a highly refactorable & composable application, as opposed to a hierarchical routing approach like routing-duplex
.
For example, an endpoint GET /persons/:id/address
would be modeled as:
getPersonAddress :: Get -> Path ("persons" / Int / "address") Int -> Aff Response
getPersonAddress _ (Path id) = ...
POST /persons
accepting a json body:
type Person = { firstName :: String, lastName :: String, age :: Maybe Int }
postPerson :: Post -> Path "persons" Unit -> ContentType Json -> Json Person -> Aff Response
postPerson _ _ _ person = ...
Then these can be rolled up into a /persons
resource with Handler.or
:
persons :: Handler Aff Response
persons = getPerson `Handler.or` postPerson `Handler.or` deletePerson `Handler.or` getPersonAddress ...
Then run with:
Axon.serveNode {port: 10000, hostname: "0.0.0.0"} persons
This example implements this REST interface in 36LoC:
GET /cheeses
- Lists all cheeses (strings) known to serverPOST /cheeses
- Add a cheese to the cheese listDELETE /cheeses/:cheese
- Remove a cheese from the cheese list
module Main where
import Prelude
import Axon as Axon
import Axon.Request.Handler as Handler
import Axon.Request.Parts.Class (Delete, Get, Path(..), Post)
import Axon.Request.Parts.Path (type (/))
import Axon.Response (Response)
import Axon.Response.Construct (Json(..), toResponse)
import Axon.Response.Status as Status
import Data.Filterable (filter)
import Data.Foldable (elem)
import Data.Tuple.Nested ((/\))
import Effect (Effect)
import Effect.Aff (Aff, launchAff_, joinFiber)
import Effect.Aff as Aff
import Effect.Class (liftEffect)
import Effect.Ref (Ref)
import Effect.Ref as Ref
main :: Effect Unit
main = launchAff_ do
cheeses :: Ref (Array String) <- liftEffect $ Ref.new
[ "cheddar", "swiss", "gouda" ]
let
getCheeses :: Get -> Path "cheeses" _ -> Aff Response
getCheeses _ _ = liftEffect do
cheeses' <- Ref.read cheeses
toResponse $ Status.ok /\ Json cheeses'
deleteCheese :: Delete -> Path ("cheeses" / String) _ -> Aff Response
deleteCheese _ (Path id) = liftEffect do
cheeses' <- Ref.read cheeses
if not $ elem id cheeses' then
toResponse Status.notFound
else
Ref.modify_ (filter (_ /= id)) cheeses
*> toResponse Status.accepted
postCheese :: Post -> Path "cheeses" _ -> String -> Aff Response
postCheese _ _ cheese =
let
tryInsert as
| elem cheese as = { state: as, value: false }
| otherwise = { state: as <> [ cheese ], value: true }
in
liftEffect
$ Ref.modify' tryInsert cheeses
>>= if _ then toResponse Status.accepted else toResponse Status.conflict
handle <-
Axon.serveBun
{ port: 8080, hostname: "localhost" }
(getCheeses `Handler.or` postCheese `Handler.or` deleteCheese)
joinFiber handle.join
Request handler functions have any number of parameters that are RequestParts
and return an Aff Response
(or any MonadAff
).
RequestParts
RequestParts
Request
- Always succeeds; provides the entire request
- Combinators
Unit
- Always succeeds
a /\ b
- Tuple of
a
andb
, wherea
andb
areRequestParts
.
- Tuple of
Maybe a
a
must beRequestParts
. Ifa
can't be extracted, the handler will still succeed and this will beNothing
. Ifa
was extracted, it's wrapped inJust
.
Either a b
a
andb
must beRequestParts
. Succeeds if eithera
orb
succeeds (preferringa
). Fails if both fail.
- Body
String
- succeeds when request has a non-empty body that is valid UTF-8
Json a
- succeeds when request has a
String
body (see above) that can be parsed intoa
usingDecodeJson
.
- succeeds when request has a
Buffer
- succeeds when request has a nonempty body.
Stream
- succeeds when request has a nonempty body.
- Headers
Header a
a
must beTypedHeader
fromAxon.Header.Typed
. Allows statically (ex.ContentType Type.MIME.Json
) or dynamically (ex.ContentType String
) matching request headers.
HeaderMap
- All headers provided in the request
- Path
Path a c
- Statically match the path of the request, and extract parameters. See
Axon.Request.Parts.Path
. (TODO: this feels too magical, maybe follow axum's prior art of baking paths into the router declaration?)
- Statically match the path of the request, and extract parameters. See
- Method -
Get
-Post
-Put
-Patch
-Delete
-Options
-Connect
-Trace
Similarly to the structural extraction of request parts; handlers can use Axon.Response.Construct.ToResponse
for easily constructing responses.
ToResponse
ToResponse
- Combinators
Status /\ a
- Special case to make sure any
Status
in a tuple will take priority over any default statuses within. TODO: This case (overlapping witha /\ b
requires the class to be "sealed" in an instance chain. Want a clean way around this so consumers can implementToResponse
.)
- Special case to make sure any
a /\ b
- Merges
toResponse a
andtoResponse b
, usingb
on conflicts
- Merges
- Status
Axon.Response.Status.Status
- Body
Axon.Response.Body.Body
String
Node.Buffer.Buffer
Node.Stream.Readable a
(for alla
)Axon.Response.Construct.Json a
a
must beEncodeJson
. This will set the body toa
stringified, and setContent-Type
toapplication/json
.
- Headers
ToResponse
is implemented for all implementors ofTypedHeader
- TODO:
Map String String