Package

purescript-axon

Repository
cakekindel/purescript-axon
License
GPL-3.0-or-later
Uploaded by
pacchettibotti
Published on
2025-05-22T14:55:46Z

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

Example

This example implements this REST interface in 36LoC:

  • GET /cheeses - Lists all cheeses (strings) known to server
  • POST /cheeses - Add a cheese to the cheese list
  • DELETE /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 Handlers

Request handler functions have any number of parameters that are RequestParts and return an Aff Response (or any MonadAff).

RequestParts

  • Request
    • Always succeeds; provides the entire request
  • Combinators
    • Unit
      • Always succeeds
    • a /\ b
      • Tuple of a and b, where a and b are RequestParts.
    • Maybe a
      • a must be RequestParts. If a can't be extracted, the handler will still succeed and this will be Nothing. If a was extracted, it's wrapped in Just.
    • Either a b
      • a and b must be RequestParts. Succeeds if either a or b succeeds (preferring a). 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 into a using DecodeJson.
    • Buffer
      • succeeds when request has a nonempty body.
    • Stream
      • succeeds when request has a nonempty body.
  • Headers
    • Header a
      • a must be TypedHeader from Axon.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?)
  • 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

  • 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 with a /\ b requires the class to be "sealed" in an instance chain. Want a clean way around this so consumers can implement ToResponse.)
    • a /\ b
      • Merges toResponse a and toResponse b, using b on conflicts
  • Status
    • Axon.Response.Status.Status
  • Body
    • Axon.Response.Body.Body
    • String
    • Node.Buffer.Buffer
    • Node.Stream.Readable a (for all a)
    • Axon.Response.Construct.Json a
      • a must be EncodeJson. This will set the body to a stringified, and set Content-Type to application/json.
  • Headers
    • ToResponse is implemented for all implementors of TypedHeader
    • TODO: Map String String
Modules
Axon.Header.Typed
Axon.Header.Typed.Cookie
Axon.Header.Typed.Parsing
Axon.Request
Axon.Request.Handler
Axon.Request.Handler.Default
Axon.Request.Method
Axon.Request.Parts.Body
Axon.Request.Parts.Class
Axon.Request.Parts.Header
Axon.Request.Parts.Method
Axon.Request.Parts.Path
Axon.Response
Axon.Response.Body
Axon.Response.Construct
Axon.Response.Status
Axon.Runtime
Axon.Runtime.Bun
Axon.Runtime.Node
Axon.Serve
Axon.Serve.Bun
Axon.Serve.Node
Axon.Web.Headers
Axon.Web.Request
Axon.Web.Response
Data.Net.SocketAddress
Data.String.Lower
Dependencies