Mig - new library to write lightweight and composable web-servers

I’m happy to announce the release of a new library for web-servers.
It offers simple and concise functions to build servers from small parts.
Servers are monoids, and we can aggregate them with functions like mconcat.

Main features:

  • lightweight library
  • expressive DSL to compose servers
  • type-safe handlers
  • handlers are encoded with generic haskell functions
  • built on top of WAI and warp server libraries.

For DSL design I wanted to touch the middle ground between scotty (being small and simple, but imperative) and servant (being composable, but somewhat advanced on type-level features)

I like scotty for being very simple and servant for being composable, type-safe and how functions are used as handlers which provides decoupling of Web-handlers from application logic. But sometimes scotty feels too imperative and lacks servant’s composability. And servant with type-level magic and huge errors can feel to complicated. So I wanted to create something in the middle. Something composable and simple at the same time. And be able to use arbitrary haskell functions as handlers.

There are only two functions to combine servers. And types of inputs and outputs are encoded with newtype-wrpappers with phantom types which provide type-safety.

It’s very first release. So feedback is welcome and I’m sure there are places to improve.
I hope that you will enjoy the lib.

links:

9 Likes

Cool! How does it compare to Twain?

1 Like

As I see in the twain it has a monad to query parameters and many of them are of unified type.
In the mig we query by providing function that takes input as argument. This idea is inspired by servant.

So instead of:

handle = do
  postId <- queryParam "a"
  traceId <- queryHeader "b"
  use postId traceId

in the mig we can write:

handle :: Query "a" PostId -> Header "b" Text -> Get IO FooResp
handle (Query postId) (Header traceId) = Get $ 
  pure (use postId traceId)  

We have newtype-wrappers like Query, Capture, Header, Body to parse request.

so we rely on standard classes for conversion: FromJSON, FromHttpApiData, ToMarkup
to get input and compose output. The handlers are encoded as haskell functions.

3 Likes

Very cool! I also like the use of Env: similar to the services design pattern I wrote a blog post on!

2 Likes

Cool. Can it be integrated with OpenAPI to generate a description?

2 Likes

At the current implementation it can not do it.
As it accumulates value:

newtype Server = Server (Req -> m (Maybe Resp))

One possible solution is to also accumulate some value Api along the way:

data Server m = Server
  { api :: Api
  , run :: Req -> m (Maybe Resp)
  }

Or instead of shallow DSL with function accumulation we can create ADT like this:

data Api a
  = AnyMethod a
  | ApiMethod Method a
  | WithBody (Json.Value -> Api a)
  | WithRawBody (BL.ByteString -> Api a)
  | WithPath [Text] (Api a)
  | ... etc

newtype Server m = Server (Api (m (Maybe Mig.Resp)))
  deriving newtype (Semigroup, Monoid)

And we can try to interpret it as Api and as Wai-application.
Actualy it was the first attempt of implementation but then I thought that accumulation of the functions is a bit easier to implement.

Here is the sketch of the types in the repo: https://github.com/anton-k/mig/blob/main/try-out-ideas/Api.hs

2 Likes

FWIW: the main reason I used Servant in my current project was because of its OpenAPI integration.

1 Like