Mig by example
Mig is a lightweight and easy to use library to build HTTP servers and clients in Haskell.
It is kind of servant for Simple/Boring Haskell.
This book is an example driven guide to the library.
The name mig
(pronounced as meeg) is a russian word for "instant moment".
The main features of the mig library are:
- lightweight library
- easy to use. It has simple design on purpose
- expressive DSL to compose servers
- type-safe route handlers, URLs and conversions
- handlers are encoded with generic Haskell functions
- built on top of WAI and warp server libraries.
- provides Swagger to your server with one-line of code
- relies on standard classes to compose servers. The server is a monoid
- we can build HTTP-clients from the server definition
Example of hello world server:
{-# Language OverloadedStrings #-}
import Mig.Json.IO
-- | Starts server on port 8085.
main :: IO ()
main = runServer 8085 server
-- | The server definition
server :: Server IO
server =
"api/v1" /.
[ "hello" /. hello
, "bye" /. bye
]
-- | The handler definition as a function
hello :: Get (Resp Text)
hello = pure $ ok "Hello World"
-- | The handler with a query parameter to ask for the user name
bye :: Query "user" Text -> Get (Resp Text)
bye (Query name) = pure $ ok ("Goodbye " <> name)
How to start a new project
If you are a Haskell beginner and interested to try out building servers with mig
the easiest way to start is to install stack
.
See the main page of the stack
docs in the link on how to do it. After the stack
is installed we can generate a
new mig
project that contains hello world server with command:
> stack new my-project-name anton-k/hello-mig
It generates my-project-name
directory that contains a code for our server.
Let's navigate to it, build server code and start the server:
> cd my-project-name
> make build
> make run
After that we can query the server on port 8085 either by curl or by swagger-ui. The project contains a basic JSON API server with two routes. The code will be explained in detail in the next chapter of this tutorial.
How to use mig library in your project
We can install it from hackage. We need to use the library mig-server
With cabal we can install it from Hackage:
cabal install mig-server --lib
With stack we can link to the repo in extra-deps
(put it in your stack.yaml
):
extra-deps:
- git: https://github.com/anton-k/mig
commit: <some-commit-of-the-mig-libray>
subdirs:
- mig
- mig-extra
- mig-client
- mig-wai
- mig-swagger-ui
- mig-server
Structure of the library
There are several libraries:
mig
- core library which defines DSL to build servers with API-schemas and functions to render it to low-level representation.mig-extra
- extra add-ons to the core librarymig-server
- mig core with batteries and functions to run servers on top of warp.mig-client
- HTTP-clients from the server codemig-wai
- convert mig servers to WAI-applicationsmig-swagger-ui
- serve swagger for you app.
Source code for examples
We are going to learn how the mig works by examples. You can run the examples from the tutorial. Here is the code that we are going to study. Look at the Makefile for commands on how to build and run the examples.
Let's dive in to the first example.
Comparing to other libraries
Why to use mig if there are other cool libraries? To me mig lies in the middle ground between servant and scotty. It's as simple as scotty and does not go to fancy type road as servant. But it is akin to servant in usage of type-safe conversions and type-level safety.
servant
The mig uses the same ideas of type-safe handlers which are based on generic Haskell functions. The main difference is that in servant the whole server is described as type. Which leads to type-safety and ability to derive API schema from the type.
But downside of it is fancy big types and very advanced concepts that user needs to know in order to use the library. Also one drawback to me is when things go wrong and you get several pages long error messages. If your server is really big it can be very hard to spot the origin of the error as type mismatch is going to be with the whole type which describes the full server.
The mig borrows idea of type-safe functions to represent route handlers. But types represent only individual handlers. It does not describe the full server. But we have type safety on the level of the single route. And error messages are going to be localised and dedicated to a single route.
Using type-level description of the routes provide the same benefits as in servant case:
- safe type check of the conversions of low level request and response elements
- usage of generic Haskell functions as handlers
- declarative design of the servers
- composition of servers from small sub-servers
In the mig API is a value that is derived from the server at run-time. It allows us to build clients and OpenApi swagger too.
To me servant is more demanding and complicated solution. I'd like to use something more simple.
scotty
The scotty is also in domain of simple, easy to use solutions. So why did I wrote mig and haven't used the scotty instead? Scotty features more imperative approach where you write handlers as expression for Scotty library monad. But it does not looks so well as in servant's case to me. It is harder to assemble servers from parts. And I really like the idea of type-safe conversions of various parts of request and response.
So the scotty is simple enough but for me it lacks some servant features such as composability of the servers (nice tree structure of the API) and type-safe conversions of various parts of request and response.
Hello world
Let's build hello world application. We are going to build simple JSON API server with single route which replies with constant text to request.
We have installed the library mig-server
. Let's import the main module.
It brings into the scope all main functions of the library:
module Main where
import Mig
Let's define a server with single route:
server :: Server IO
server = "api/v1/hello" /. hello
hello :: Get IO (Resp Json Text)
hello = undefined
So we serve single route with path "api/v1/hello"
.
This example relies on extension OverloadedStrings
to convert
string literals to values of Path
type. Usually I add it in the cabal file of
the project.
Let's cover the types first.
The server type
The server is a description of both OpenAPI schema for our server and low-level function to run it. In the library it is a newtype wrapper:
newtype Server m = Server (Api (Route m))
The Api
type is a value to describe the API schema and Route
contains
useful info on the type of the route (method, description of the inputs and outputs)
and how to run the handler function.
The server is parametrized by some monad type. For this example we use IO
-monad.
It means that all our handlers are going to return IO
-values.
How to link paths to handlers
To bind path "api/v1/hello" to handler hello
we use function (/.)
. Let's look at it's type signature:
(/.) :: ToServer a => Path -> a -> Server (MonadOf a)
It expects the Path
which has instance of class IsString
that is why we can
use plain strings for it. The second argument is something that is convertible to Server
.
Here we use trick to be able to use arbitrary Haskell functions as handlers.
We have special class called ToServer
which can convert many different types to Server
.
The output type is a bit tricky: Server (MonadOf a)
.
The MonadOf
is a type function which can extract m
from (Server m)
.
Or for example it can extract m
from the function request -> m response
.
So the MonadOf
is a way to get underlying server monad from any type.
Let's be more specific and study our example.
The type of the handler is Get IO (Resp Text)
In our case we get:
(/.) :: Path -> Get IO (Resp Text) -> Server IO
The type-level function MonadOf
knows how to extract IO
from Get IO (Resp Text)
.
The type of response
Let's study the signature of the hello
handler:
hello :: Get IO (Resp Json Text)
| | | | |
| | | | +-- response body converted to byte string
| | | |
| | | +---- codec to convert result to response body
| | | (the media-type which the route uses for response body)
| | |
| | +---- type of response which holds HTTP-response info with result
| |
| +----- the server monad. Our handler returns values in this monad
|
+----- http method encoded as a type
The type Get
is a synonym for more generic Send
type:
type Get m a = Send GET m a
The type Send
is just a wrapper on top of monadic value:
newtype Send method m a = Send (m a)
It encodes HTTP-method on type level as so called phantom type. This is useful to aggregate value for API-schema of our server.
We have type synonyms for all HTTP-methods (Get
, Post
, Put
etc).
It's interesting to know that library mig does not use any custom monads for operation.
Instead it runs on top of monad provided by the user. Usually it would be IO
or Reader
over IO
.
Also for convenience Send
is also Monad
, MonadTrans
and MonadIO
.
So we can omit Send
constructor in many cases.
HTTP-response type
Let's study the Resp
type. It is a type for HTTP response.
It contains the value and additional HTTP information:
-- | Response with info on the media-type encoded as type.
data Resp media a = Resp
{ status :: Status
-- ^ response status
, headers :: ResponseHeaders
-- ^ response headers
, body :: Maybe a
-- ^ response body. Nothing means "no content" in the body
}
deriving (Show, Functor)
The type argument media
is interesting. It gives a hint to the compiler on how
to convert the body to low-level byte string representation.
In our example we use type-level tag Json
to show that we are going to convert
the result to JSON value in the response. So in our case of Resp Json Text
we are going to return Text
which will be converted to JSON value.
To return successful response there is a handy function:
ok :: a -> Resp media a
It returns response with 200 ok-status and sets Content-Type
header to proper media-type.
Define a handler
Let's complete the example and define a handler which returns static text:
hello :: Get IO (Resp Json)
hello = Send $ pure $ ok "Hello World!"
We have several wrappers here:
ok
- converts text value to http-responseResp Json Text
pure
- converts pure value to IO-based valueSend
- send converts monadic value to server. It adds information on HTTP-method of the return type.
As Send
is also monad if m
is a monad we can write this definition
a bit shorter and omit the Send
constructor:
hello :: Get IO (Resp Json)
hello = pure $ ok "Hello World!"
Run a server
Let's run the server with warp. For that we define the main
function for our application:
main :: IO ()
main = do
putStrLn $ "Server starts on port: " <> show port
runServer port server
where
port = 8085
That's it! We can compile the code and run it to query our server.
We use the function runServer
:
runServer :: Int -> Server IO -> IO ()
It renders our server to WAI-application and runs it with warp.
Complete code for the example
module Main (main) where
import Mig
main :: IO ()
main = do
putStrLn $ "Server starts on port: " <> show port
runServer port server
where
port = 8085
server :: Server IO
server = "api/v1/hello" /. hello
hello :: Get IO (Resp Json Text)
hello = pure $ ok "Hello World!"
If we run the code we can test it with curl
in command line:
> curl http://localhost:8085/api/v1/hello
"Hello World!"
Add more routes
Let's define another handler to say bye
:
bye :: Get IO (Resp Json)
bye = pure $ ok "Goodbye"
We can add it to the server with monoid method as Server m
is a Monoid
:
server :: Server IO
server =
"api/v1" /.
mconcat
[ "hello" /. hello
, "bye" /. bye
]
The meaning of the monoid methods for Server
:
mempty
- server that always fails on any requesta <> b
- try to serve the request with servera
if it succeeds return the result. If it fails try to serve with serverb
.
So we have just two functions to build nested trees of servers:
path /. server
- to serve the server on specific pathmconcat [a, b, c, d]
- to combine several servers into one
Note that we can have several handlers on the same path if they have different methods or media-types for output or input:
server =
"api/v1" /.
mconcat
[ "hello" /. helloGet
, "hello" /. helloPost
]
helloGet :: Get IO (Resp Json Text)
helloPost :: Post IO (Resp Json Text)
Servers on the same path are also distinguished by:
- http-method
- media-type of the result (value of "Accept" header)
- media-type of the request (value of "Content-Type" header)
Subtle nuance on Monoid instance for Server
You may ask: why not to write the previous example like this:
server =
"api/v1/hello" /.
mconcat
[ helloGet
, helloPost
]
There is a subtle nuance here. The Server m
is a Monoid
.
But the value Send method m a
is not. So we use the function (/.)
which converts the second argument to Server
. If we want to convert
we can use the method of the class ToServer
:
toServer :: ToServer a => a -> Server (MonadOf a)
So the right way to avoid duplication in path is:
server =
"api/v1/hello" /.
mconcat
[ toServer helloGet
, toServer helloPost
]
Regarding the previous example we could not use mconcat
even if we wanted to.
Because handelGet
and handlePost
have different types. They can not
be even put in the same list. But here lies the beauty of the library.
We can use arbitrary types as handlers but in the end they all get converted
to the value Server m
. So we have the flexibility on DSL level but
on the level of implementation to build the tree of handlers we use the same type.
Which makes type very simple.
List instance for Servers
Because of the ToServer a => ToServer [a]
instance we can omit the mconcat
most of the time. Meaning we can write the previous examples as:
server =
"api/v1/hello" /.
[ toServer helloGet
, toServer helloPost
]
Also for example with paths for alternatives in the list we can omit toServer
too:
server =
"api/v1" /.
[ "hello" /. hello
, "bye" /. bye
]
The path type
Let's discuss the Path
type.
It is a list of atomic path items:
newtype Path = Path [PathItem]
deriving (Show, Eq, Semigroup, Monoid)
The path item can be of two types:
data PathItem
= StaticPath Text
| CapturePath Text
The static path item is a rigid entity with exact match to string. We have used it in all our examples so far. But capture is wild-card which is going to be used as input to the handler.
To construct only rigid paths we can use strings:
"ap1/v1/get/blog/post"
"foo/bar"
To specify captures we use *
-wildcard:
api/v2/*/get
In the star mark the request captures any text. There might be as many stars in the path as you wish. But they should be supported by the handler. We will touch upon that later.
It's good to know that path is a special type which can be constructed from strings
(use OverloadedStrings
extension). And we can two types of atomic path elements.
Static items and capture parameters. We will deal with captures in the next example.
Anatomy of the request
For the next example we are going to try all sorts of inputs which are possible for the handler.
Useful presets for servers
Before we dive into various parts of the handler I'd like to introduce couple of
useful modules that make servers more specific. Often we don't need the most generic types.
If we know that all our servers will serve JSON and use only IO monad
we can use a special version of the Mig
module:
import Mig.Json.IO
It will provide several wrappers to simplify type signatures for handlers:
type Get a = Send GET IO a
type Post a = Send POST IO a
Also it provides more specific response type:
newtype Resp a = Resp (Core.Resp Json a)
For the next example we are going to build JSON-application again.
So instead of more general Mig
we will use Mig.Json.IO
.
Also there are similar modules for:
IO
-based serversHtml
servers with generic monadJson
servers with generic monadJson+IO
serversHtml+IO
servers
Servers for HTML take one step further and remove Resp
from the equation:
type Get a = Send GET IO (Resp Html a)
type Post a = Send POST IO (Resp Html a)
There is one reason why we do not do that for JSON. But we will study it later.
Http request
In previous example we could query by static path. Let's do something more fancy and provide the input for the handler.
We have several types of inputs in HTTP:
-
query parameters. We can see them in the path
"api/get/route?queryName=queryValue"
-
capture parameters. We can see them also in the path, but they are inlined right into it:
api/get/route/someCaptureValueA/someCaptureValueB
-
header parameters. They are in HTTP-request headers. For example header that reports media-type of the request body:
"Content-Type: application/json"
-
request body. It is a value packed into HTTP-request. It can be JSON or text or raw string or XML. All sorts of things can be used as request bodies.
To use any of HTTP inputs in the handler we use special newtype
wrappers
as arguments to the handler functions.
Query parameter example
For example let's alter hello
handler to greet not the "World"
but someone
by the name:
hello :: Query "who" Text -> Get (Resp Text)
hello (Query name) =
pure $ ok $ "Hello " <> name
Note that we have imported Mig.IO.Json
and our types are more
specific and have fewer arguments. All types are dedicated to IO
and Json
.
So we can write Get (Resp Text)
instead of Get IO (Resp Json Text)
.
Interesting part of the handler is that argument: Query "who" Text
.
On the API level it creates expectation for a required query parameter in the path.
The Query
is a simple newtype
wrapper:
newtype Query name value = Query value
The cool part of it is that code for the server does not change:
server :: Server IO
server = "api/v1/hello" /. hello
There is no change because function (/.)
is overloaded by second argument.
And it accepts all sorts of inputs. One of them states:
if value
a
is convertible to server thenQuery name value -> a
is also convertible to server
And by this magic as all Haskell functions are curried we can use any number of queries in the handler. For example if we want to greet two persons we can write:
hello :: Query "personA" Text -> Query "personB" Text -> Get (Resp Text)
hello (Query nameA) (Query nameB) =
pure $ ok $ "Hello " <> nameA <> " and " <> nameB
Also we can input any type if it has instance of the classes FromHttpApiData
and ToParamSchema
.
For example let's add two numbers:
add :: Query "a" Int -> Query "b" Int -> Get (Resp Int)
add (Query a) (Query b) =
pure $ ok (a + b)
The rest of the inputs
All other input parameters work in the same way as a Query
. We have a newtype
wrapper
for the value and type denotes all useful info for API description of the handler.
Let's for example query numbers for addition as capture parameters:
add :: Capture "a" Int -> Capture "b" Int -> Get (Resp Int)
add (Query a) (Query b) =
pure $ ok (a + b)
It will expect the path to be "api/v1/add/2/4"
.
Other wrappers look very similar:
Header name value
- for required headersOptionalHeader name value
- for optional headersCapture name value
- for path capturesOptional name value
- for optional queriesQueryFlag
- for boolean query that can be missing in the path (and then it isfalse
)Body media value
- for request bodyCookie
- for cookie (set in the header)
Using custom types as query parameters
The value of query parameter should have two instances of classes. We need:
-
FromHttpApiData
from the libraryhttp-api-data
to convert to value from piece of the URL. -
ToParamSchema
from the libraryopenapi3
to describe parameter type in the OpenApi schema.
Let's create a custom type and provide those instances:
newtype AuthToken = AuthToken Text
deriving newtype (FromHttpApiData, Eq, Ord, ToParamSchema)
We can derive them for newtype
wrappers. After that we can use AuthToken
as value
to get from query parameter. For more info on how to derive those instances see the docs for the libraries.
It's easy to do. We can derive Generic
for the data type and derive ToParamSchema
with it.
The same instances we need for all parameters-like inputs: queries, headers, captures.
Nuances for Capture
The capture is interesting because it can be anywhere in the path. For the example we haven't altered the server and our example:
add :: Query "a" Int -> Query "b" Int -> Get (Resp Int)
add (Query a) (Query b) =
pure $ ok (a + b)
server = "api/v1/add" /. add
The server expects strings with template:
api/v1/add/{int}/{int}
So for missing captures it inserts them. It is the same as to write:
server = "api/v1/add/*/*" /. add
We denote capture with *
-wildcard. If we want the capture to be in another place
in the path just put a star there:
server = "api/v1/*/*/add-me" /. add
The server expects strings with template as path:
api/v1/{int}/{int}/add-me
Json request body
I guess that JSON body as request is going to be the most popular case among all inputs. So let's take a closer look at it as it often requires the custom type.
Let's add two numbers and provide input with request body:
data AddInput = AddInput
{ a :: Int
, b :: Int
}
deriving (Generic, FromJSON, ToSchema)
-- | Using JSON as body request
handleAddJson :: Body AddInput -> Post (Resp Int)
handleAddJson (Body (AddInput a b)) =
pure $ ok $ a + b
In the core mig library the type Body
has two type arguments. But as we use Json specification
the first argument for Mig.Json.IO
as for Mig.Json
is always Json
-tag.
So those modules provide special case alternative for type Body
. But in the mig
library it uses the same idea as we saw in the query parameter. It is just a
newtype
wrapper for the value.
To be able to use it as input for the handler we have to provide instances for several types:
FromJSON
fromaeson
library to parse value as JSON from byte stringToSchema
fromopenapi3
library to describe it in the API-schema
Both of the types can be easily derived with Generic
instance (from the module GHC.Generics).
First we derive instance of the Generic
and then we can derive both FromJSON
and ToSchema
:
data AddInput = AddInput
{ a :: Int
, b :: Int
}
deriving (Generic, FromJSON, ToSchema)
Also there are many libraries on Hackage to
create custom drivings for those classes: deriving-aeson
, aeson-deriving
and many others.
So to use JSON request body we can define our own type, derive proper classes and we are done.
Let's build a server
Let's recap on what we have learned and build server with various request inputs:
module Main (main) where
import Mig.Json.IO
main :: IO ()
main = runServer 8085 server
-- | Let's define a server
server :: Server IO
server =
"api" /.
-- no args, constnat output
[ "hello/world" /. helloWorld
, -- required query param and custom header
"succ" /. handleSucc
, -- optional query param
"succ-opt" /. handleSuccOpt
, -- several query params
"add" /. handleAdd
, -- query flag
"add-if" /. handleAddIf
, -- capture
"mul" /. handleMul
, -- json body as input
"add-json" /. handleAddJson
]
-- | Simple getter
helloWorld :: Get (Resp Text)
helloWorld = do
pure $ ok "Hello world!"
newtype TraceId = TraceId Text
deriving newtype (FromHttpApiData, ToHttpApiData, ToText, ToParamSchema)
{-| Using several inputs: header argument and required query
and using conditional output status
-}
handleSucc :: Header "Trace-Id" TraceId -> Query "value" Int -> Get (Resp Int)
handleSucc (Header _traceId) (Query n) =
pure $ ok (succ n)
-- | Using optional query parameters.
handleSuccOpt :: Optional "value" Int -> Get (Resp Int)
handleSuccOpt (Optional n) =
pure $ case n of
Just val -> ok (succ val)
Nothing -> ok 0
{-| Using several query parameters
-}
handleAdd :: Query "a" Int -> Query "b" Int -> Get (Resp Int)
handleAdd (Query a) (Query b) =
pure $ ok $ a + b
-- | Using query flag if flag is false returns 0
handleAddIf :: Query "a" Int -> Query "b" Int -> QueryFlag "perform" -> Get (Resp Int)
handleAddIf (Query a) (Query b) (QueryFlag addFlag) = do
pure $
ok $
if addFlag
then (a + b)
else 0
{-| Using capture as arguments. This route expects two arguments
captured in URL. For example:
> http://localhost:8085/hello/api/mul/3/100
-}
handleMul :: Capture "a" Int -> Capture "b" Int -> Get (Resp Int)
handleMul (Capture a) (Capture b) = do
pure $ ok (a * b)
data AddInput = AddInput
{ a :: Int
, b :: Int
}
deriving (Generic, ToJSON, FromJSON, ToSchema)
-- | Using JSON as input
handleAddJson :: Body AddInput -> Post (Resp Int)
handleAddJson (Body (AddInput a b)) =
pure $ ok $ a + b
Curls to test the routes:
curl http://localhost:8085/api/hello/world
curl -X 'GET' \
'http://localhost:8085/api/succ?value=2' \
-H 'accept: application/json' \
-H 'Trace-Id: xyz-trace'
curl -X 'GET' \
'http://localhost:8085/api/add-if?a=2&b=4&perform=true' \
-H 'accept: application/json'
curl -X 'GET' \
'http://localhost:8085/api/mul/100/23' \
-H 'accept: application/json'
Adding some goodies to the servers
There are some useful add-ons that make development of the servers much more pleasant. Let's discuss couple of them.
Add swagger
Making curl
request can quickly become hard to manage as
our servers become more complicated. There is OpenAPI standard
that defines how to describe HTTP-server API. Also it provides
Swagger. It is a tool to make it easy to check how server behaves.
It provides an HTTP-client for the server usable from the browser as plain web-page which allows us to
query server routes.
Let's add a swagger to our server. Just add this line:
server :: IO
server =
withSwagger def $
"api" /. [ {- the rest of the code -} ]
Let's add this line to our example and restart the server. By default it creates a route for the server that serves Swagger UI client at the path: http://localhost:8085/swagger-ui/. It is easy to query the routes with swagger ui.
We can add swagger to any server with function:
withSwagger :: SwaggerConfig m -> Server m -> Server m
We will study the SwaggerConfig
in details in one of the next chapters
but for now the default value which is set with def
from library data-default
is fine.
Add simple logs to the server
We can look at the request and response data with tracing functions
which come from library mig-extra
from the module Mig.Extra.Plugin.Trace
:
data Verbosity = V0 | V1 | V2 | V3
-- log http requests and responses
logHttp :: Verbosity -> Plugin m
-- | log requests
logReq :: Verbosity -> Plugin m
-- | Log responses
logResp :: Verbosity -> Plugin m
The Plugin m
is a function that can be applied to all routes of the server
and modify their behavior. To apply plugin to server we can use functions:
applyPlugin :: Plugin m -> Server m -> Server m
($:) :: Plugin m -> Server m -> Server m
We show simplified signatures here. The real ones are overloaded by the first argument. But we will discuss plugins in depth in the separate chapter. For now it's ok to assume that those functions are defined in that simplified way.
So let's look at the data that goes through our server:
import Mig.Extra.Plugin.Trace qualified as Trace
...
server =
withSwagger def $
withTrace $ {-# the rest of the server code #-}
where
withTrace = applyPlugin (Trace.logHttp Trace.V2)
Let's restart the server and see what it logs:
log:
body: ''
headers:
accept: application/json
method: GET
path: api/add?a=12&b=45
time: 2023-10-05T16:29:16.262934Z
type: http-request
log:
body: 57
duration: 9.750000000000001e-4
headers:
content-type: application/json
method: GET
path: api/add?a=12&b=45
status: 200
time: 2023-10-05T16:29:16.263903Z
type: http-response
This is an easy way to add add hock logs to the application. Note that those logs are not aware of concurrency and will report intermingled messages on concurrent queries.
We can add real logs with more generic versions of the functions
which accept callback and we can pass the logger function defined in terms
of one of the standard Haskell logging libraries, say katip
or fast-logger
:
import Data.Aeson as Json
logHttpBy :: (Json.Value -> m ()) -> Verbosity -> Plugin m
Summary
We have learned how various parts of the requests can be queries
with newtype
wrappers. There are only handful of them.
We can query
Query name value
- for required queriesBody media value
- for request bodyOptional name value
- for optional queriesHeader name value
- for required headersOptionalHeader name value
- for optional headersCookie value
- for cookies (set in the header)Capture name value
- for path capturesQueryFlag
- for boolean query that can be missing in the path (and then it isfalse
)
We have learned to use specialized versions for servers which operate
only in terms of IO
or Json
. We can import the module Mig.Json.IO
and our signatures would become more simple and specific.
We have learned how by one-liners we can add to the server some useful features:
-
swagger:
(withSwagger def server)
For calls to the server in the UI -
trace logs:
(applyPlugin (logHttp V2))
To see the data that flows through the server
Both expressions transform servers and have signatures:
Server m -> Server m
Anatomy of the response
For the next example we are going to study which outputs can handler produce. Let's study the HTTP-response.
Http response
We already have seen the Resp
data type in the first chapter:
-- | Response with info on the media-type encoded as type.
data Resp media a = Resp
{ status :: Status
-- ^ response status
, headers :: ResponseHeaders
-- ^ response headers
, body :: Maybe a
-- ^ response body. Nothing means "no content" in the body
}
deriving (Show, Functor)
It is the main type to return values and additional HTTP-information from response.
An HTTP-response contains:
- integer status. It's 200 when everything is alright
- list of headers which provide useful info on response type
- the byte string body which contains result of handler operation. It can hold JSON, HTML, plain text, raw byte string and other types of outputs.
In the Resp
type the media
type argument specifies which
type the body has. By this type handler knows how to convert value
to low-level byte string representation.
When things go bad
Sometimes things go bad and we would like to send errors and
state in the status the type of the error. To report errors
we have special type RespOr
:
-- | Response that can contain an error. The error is represented
-- with left case of an Either-type.
newtype RespOr ty err a = RespOr {unRespOr :: Either (Resp ty err) (Resp ty a)}
So this value has two possible responses which share the same media type. We need two different responses to be able to report errors with different type than the type of the result.
Response type class IsResp
To unify the output for both cases of Resp
and RespOr
we have special type class called IsResp
for
all types which can be converted to low-level HTTP-response type Response
.
Let's study this type class.
It has two associated types for the type of the body (RespBody
) and type of the error (RespError
):
class IsResp a where
type RespBody a :: Type
type RespError a :: Type
type RespMedia a :: Type
We can return successful result with method ok
:
-- | Returns valid repsonse with 200 status
ok :: RespBody a -> a
When things go bad we can report error with method bad
:
-- | Returns an error with given status
bad :: Status -> RespError a -> a
Sometimes we do not want to return any content from response. We can just report error status and leave the body empty:
-- | response with no content
noContent :: Status -> a
We can add custom headers to the response by method addHeaders
:
-- | Add some header to the response
addHeaders :: ResponseHeaders -> a -> a
Note that header Content-Type
is set automatically. Although sometimes
we would like set it explicitly. For that we have the method:
-- | Set the media type of the response
setMedia :: MediaType -> a -> a
Also we can set response status with function:
-- | Set the response status
setStatus :: Status -> a -> a
Also the core of the class is the method to convert value to low-level response:
-- | Converts value to low-level response
toResponse :: a -> Response
Both Resp
and RespOr
are instances of IsResp
class and
we can Send
as HTTP-response anything which has instance of IsResp
.
For now there are only three types. The third one instance is the low-level Response
type.
Examples
So we use Resp
if we are sure that handler always produce a value
and we use RespOr
if handler can produce and error.
How to return error
We already have seen many usages of Resp
type. Let's define something
that can produce an error. Let's define server that calculates
square root of the value. For negative numbers it is not defined in the
domain of real numbers. So let's define the handler that use RespOr
type:
import Mig.Json.IO
server :: Server IO
server =
"square-root" /. squareRoot
squareRoot :: Body Float -> Post (RespOr Text Float)
squareRoot (Body arg) = pure $
if arg >= 0
then ok (sqrt arg)
else bad badRequest400 "Argument for square root should be non-negative"
So we return error message and bad request status 400 when negative argument is passed to the handler.
Also note this function looks like pure GET-type function but by the HTTP rules we can not have body request in the GET-method. So we use POST instead.
Also we have special case function for bad requests called badReq
. The
values for status come from the library http-types. See the module dedicated
to HTTP-statuses.
It is reexported by the mig
library.
How to set headers
For example in the Header we expect trace id with which we can
find the request and response in the logs. And we want to pass the
trace id from request to the response. Let's do it with addHeaders
:
passTrace :: Header "trace-id" Text -> Post (Resp ())
passTrace (Header traceId) =
pure $ addHeaders [("trace-id", toHeader traceId)] $ ok ()
The function toHeader
is re-exported from the library http-api-data
.
It converts various values to header byte string.
Also there is a function if we want to add only one header and not a list of them:
setHeader :: (IsResp a, ToHttpApiData h) => HeaderName -> h -> a -> a
It has toHeader
built into it.
Just like we set headers we also can set HTTP-status of the response.
We just apply it to Resp-like value. It works both for Resp
and RespOr
:
setStatus :: IsResp a => Status -> a -> a
Although we rarely need this function as ok
sets the right status
for successful response and all functions that need the status take it as argument.
Also we have functions to set cookies that are form url-encoded:
setCookie :: (ToForm val, IsResp a) => SetCookie val -> a -> a
-- sets cookie params
data SetCookie
-- | Cookie setter with default params (only value)
defCookie :: val -> SetCookie val
defCookie = ...
For great explanation on how cookies work in HTTP you can read an article.
Under the hood it is just a http-header with name SetCookie
.
To read the cookie value use input request newtype
-wrapper Cookie
.
How it works with server definition
How can we use both of the types as responses: Resp
and RespOr
?
Recall that /.
function is overloaded by the second argument and
we have a rule for ToServer
class that:
if
a
hasIsResp
instance thenSend method m a
is convertible to server
As for both Resp
and RespOr
the instance for IsResp
is defined we can use
both types as a result of the HTTP-handler.
Summary
We have learned that there are only tow types to return from server handler:
Resp
for handlers that always produce a valueRespOr
for handlers that can fail
The need for the second type is to have different type of the error
and different for the result. If both error and result have the same
type then we can use Resp
. This is common case for HTML servers when we
return HTML-page as a result. In the case of error we would like to show the page too
as in the case of success. The difference would be in the HTTP-status of the response.
And this goes well with IsResp
class as for Resp media a
error type RespError
equals to a
as the value for RespBody
too.
Also we have learned various methods of the IsResp
class and how they
can be useful in server definitions.
See the source code RouteArgs
for examples on the topic that we have just studied.
With this chapter we have covered both requests and responses and which types the can have. It covers all basics of the mig library. You are now well equipped to build HTTP-servers in Haskell. The rest of the tutorial covers more advanced features of the library:
- how to use custom monads. So far we used only plain
IO
-monad - how to use plugins/middlewares to add common procedures to all handlers of the server
- how to create HTTP-clients from servers
- description of two more substantial examples
- JSON API application for weather forecast
- HTML example for blogpost site
Using other monads with Server
So far we have seen only IO
monad to be used with Server
.
But we can use other monads with it. Although so far
only three types of monads are supported for Servers
:
IO
-monadReaderT env IO
andnewtype
wrappers on top of itReaderT env (EitherT IO)
and new type wrappers on top of it
So the library is limited in monad choice but all of the cases can cover everything you need from the server.
Also we can use any monad which is convertible to IO
with function:
hoistServer :: forall a . (m a -> n a) -> Server m -> Server n
The reason why we would like to convert to IO
because warp server
convertion function runServer
works only for the type Server IO
.
So we can use any monad but we would like to convert to IO
at the very and
to be able to run our server with warp.
I personally prefer to just use IO
and pass environment around
to handlers. This process can be automated with ReaderT
monad.
Let's study how to use ReaderT
with the server.
As example we will build a server that contains mutable state.
It has internal counter which we can query and increment.
To store the internal state as shared environment for all handlers
we are going to Reader-pattern or server with ReaderT
over IO
monad.
Reader-pattern
Our server is Json-based but we want custom monad. So we can import the preset module
for Json
:
import Mig.Json
Also we import ReaderT
from mtl
library and IORef
to store mutable shared state:
import Control.Monad.Reader
import Data.IORef
Let's define a type for our application:
newtype App a = App (ReaderT Env IO a)
deriving newtype (Functor, Applicative, Monad, MonadReader Env, MonadIO, HasServer)
{-| Common shared state
We can put more shared state if we need. Like logger state or some interfaces.
-}
data Env = Env
{ current :: IORef Int
}
-- | Init shared state
initEnv :: IO Env
initEnv = Env <$> newIORef 0
HasServer
class
We declare it as newtype
-wrapper with ReaderT
under the hood.
We can derive all the classes that we need to use it as Reader
.
All classes but last are common repertoire of the Haskell.
The last class HasServer
is special to mig
library.
It can be also auto-derived as the instance for ReaderT+IO
is already defined.
The HasServer
class defines how to convert our special monad m server
to Server IO
. For a reader it defines a method:
renderServer :: Server (ReaderT env m) -> env -> IO (Server IO)
So if we pass the common shared environment env
to server
we can use it as Server IO
. We need to convert to Server IO
because for WAI and warp we can run only Server IO
based servers.
As in library mig-wai
:
toApplication :: Server IO -> Wai.Application
How to run Reader based server
So to run the ReaderT
server we need to convert it to IO
-based server
and we can run it with usual runServer
function:
main :: IO ()
main = do
env <- initEnv
putStrLn ("The counter server listens on port: " <> show port)
runServer port $ withSwagger def $ renderServer server env
where
port = 8085
server :: Server App
Here we also add the swagger to the server for easy testing and trying things out with swagger.
Server with Reader monad
Our server has two routes:
get
- to query current stateput
- to add some integer to the state
server :: Server App
server =
"counter"
/. [ "get" /. handleGet
, "put" /. handlePut
]
Let's define the get
route:
-- | Get handler. It logs the call and returns current state
handleGet :: Get App (Resp Int)
handleGet = Send $ do
logInfo "Call get"
ref <- asks (.current)
liftIO $ ok <$> readIORef ref
-- | Helper to do simple logging
logInfo :: String -> App ()
logInfo message = liftIO $ putStrLn $ "[INFO] " <> message
So we ask for the common mutable state and read it with readIORef
function.
Also we use liftIO
to lift IO
result to App
monad.
We just use App
monad inside Send
-wrapper to create a handler.
Let's define the put
handler:
-- | Put handler. It logs the call and updates
-- the state with integer which is read from URL
handlePut :: Capture "arg" Int -> Post App (Resp ())
handlePut (Capture val) = Send $ do
logInfo $ "Call put with: " <> show val
ref <- asks (.current)
liftIO $ ok <$> atomicModifyIORef' ref (\cur -> (cur + val, ()))
We use atomicModifyIORef'
to be safe in presence of concurrent requests.
So we have completed the definition and we can run the app and try it out.
You can find the complete code of the example in the mig
repo.
Using custom monad
We have studied how to use ReaderT IO
and newtype
-wrappers on top of it
as monads for our server. To use any other monad we need to have the function:
runAsIO :: MyMonad a -> IO a
For custom monad MyMonad
. If there is such a function we can use function:
hoistServer :: forall a . (m a -> n a) -> Server m -> Server n
Prior to call to runServer
and run the server which is based on our custom monad:
main :: IO ()
main = runServer 8085 (hoistServer runAsIO server)
server :: Server MyMonad
server = ...
Summary
In this chapter we have learned how to use Reader-monad with mig
library.
We can define our custom wrapper for ReaderT+IO
and derive instance
of HasServer
and we are ready to go.
Plugins
A plugin is a transformation which is applied to all routes in the server.
Also often it is called a middleware. But here we use a bit shorter name for it
and call it a Plugin
.
It is a pair of functions which transform API-description and server function:
data Plugin m = Plugin
{ info :: RouteInfo -> RouteInfo
-- ^ update api schema
, run :: PluginFun m
-- ^ update server function
}
-- | Low-level plugin function.
type PluginFun m = ServerFun m -> ServerFun m
To apply plugin to server we ca use function applyPlugin
:
-- | Applies plugin to all routes of the server.
applyPlugin :: forall f. (ToPlugin f) =>
f -> Server (MonadOf f) -> Server (MonadOf f)
There is also infix operator for application ($:)
.
The class ToPlugin
contains all types that can be converted to plugin.
Here we use the same trick as with ToServer
class to be able to read type-safe parts of the request
and update the API-schema. The type-level function MonadOf
knows how to find underlying monad m
in various types.
We have recursive set of rules for types that can be converted to Plugin
:
The identity rule:
PluginFun
has instance ofToPlugin
with obvious identity instance
Recursive steps for inputs
if
f
isToPlugin
then(Query name queryType -> f)
isToPlugin
too
And so on for other types of request input (query parameters, headers, captures, request bodies).
See the full list of instances in the module Mig.Core.Class.Plugin
.
Examples
So the plugin allows us to apply some behavior to all routes in the server. Let's discuss some examples
Add logging
Let's add the logging to all methods which are called. We will log which route was called and we will include the time stamp and the full path in the log:
Let's imagine that we have a function
logInfo :: Text -> IO ()
We can query the path with FullPathInfo
newtype
:
newtype FullPathInfo = FullPathInfo Text
And we have a rule for ToPlugin
class:
if
f
isToPlugin
then(FullPathInfo -> ToPlugin f)
isToPlugin
So we can create a plugin function:
logRoutes :: Plugin IO
logRoutes = toPlugin $ \(FullPathInfo path) -> prependServerAction $ do
now <- getCurrentTime
logInfo $ mconcat
[ "Call route: ", path
, " at ", Text.pack (show now)
]
We use function prependServerAction
that creates a Plugin
from action which is performed prior to call to server function:
prependServerAction :: MonadIO m => m () -> Plugin m
Also there are similar functions in the module: appendServerAction
and processResponse
.
Allow only secure routes
Another great example of plugin at work is to block routes on some conditions.
For example if we want certain routes to be used only under secure SSL connection.
We have a standard function for that whenSecure
. But let's dive into it's definition to
see how plugins can be used:
-- | Execute request only if it is secure (made with SSL connection)
whenSecure :: forall m. (MonadIO m) => Plugin m
whenSecure = toPlugin $ \(IsSecure isSecure) ->
processResponse (if isSecure then id else const (pure Nothing))
Here we use standard plugin processResponse
which allows
us to alter the result of the HTTP-response:
processResponse :: MonadIO m =>
(m (Maybe Response) -> m (Maybe Response)) -> Plugin m
Also we use query input IsSecure
which is true if connection is made over SSL:
newtype IsSecure = IsSecure Bool
So we pass through the response with identity if connection is secure
and we block the execution by returning Nothing
if connection is secure.
The cool part of it is that due to laziness there is no performance overhead and underlying
route is not going to be performed if connection is insecure.
Authorization with plugin
Let's use this schema for authorization to site. There is a route that provides authorized users with session tokens. A user can pass credentials as request body over secure connection and get session token in response which is valid for some time.
With that token the user can access the rest of the application. The user can pass token as a special header. And we check in the application that token is valid.
Imagine that we have a type for a session token:
newtype AuthToken = AuthToken Text
deriving newtype
(ToJSON, FromJSON, FromHttpApiData, Eq, Ord, Show, ToParamSchema, ToSchema)
And we can get it from some route:
getToken :: Body UserCreds -> Post (Resp AuthToken)
We would like to block invalid sessions for all routes of our site.
We can create it in similar way as whenSecure
:
isValid :: AuthToken -> IO Bool
isValid = ...
headerAuth :: Header "auth" AuthToken -> Plugin IO
headerAuth (Header token) = processResponse $ \getResp -> do
isOk <- isValid token
if isOk
then getResp
else pure $ Just $ bad badRequest400 "Auth token is invalid"
whenAuth :: Server IO -> Server IO
whenAuth = applyPlugin headerAuth
In this example we use IsResp
instance for low-level http Response
to report authorization error. The header with name "auth"
is required
for all routes which are part of the server to which we apply the plugin.
Summary
In this chapter we have learned on plugins. They provide a tool to apply transformation to all routes in the server. Which can be useful for logging, authorization and adding common behavior to all routes.
How to use Swagger
The Swagger is a powerful tool to try out your servers. It provides easy to use Web UI to call routes in the server. We already have seen how to augment server with swagger. It is just a line of code:
withSwagger def server
The function withSwagger
is defined in the package mig-swagger-ui
which is re-exported by mig-server
.
In this chapter we are going to learn how to tweak and fine-tune the swagger.
Swagger config
We have used default swagger config with constant def
.
Let's look at what can be configured:
data SwaggerConfig m = SwaggerConfig
{ staticDir :: Path
-- ^ path to server swagger (default is "/swagger-ui")
, swaggerFile :: Path
-- ^ swagger file name (default is "swaggger.json")
, mapSchema :: OpenApi -> m OpenApi
-- ^ apply transformation to OpenApi schema on serving OpenApi schema.
-- it is useful to add additional info or set current date in the examples
-- or apply any real-time transformation.
}
instance (Applicative m) => Default (SwaggerConfig m) where
def =
SwaggerConfig
{ staticDir = "swagger-ui"
, swaggerFile = "swagger.json"
, mapSchema = pure
}
We can set in staticDir
at what path to serve the swagger,
how to name the swagger file with swaggerFile
and
which run-time transformation we apply to the OpenApi schema.
We can use this mapping to add useful description to the app
or keep examples up to date if they use for example timestamps
that should be in the future to be valid.
Add description to application
Often we would like to provide short description of the application,
which version it has. We can use the package openapi3
to update OpenApi
directly. But also there is a helper type for most often used fields:
-- | Default info that is often added to OpenApi schema
data DefaultInfo = DefaultInfo
{ title :: Text
, description :: Text
, version :: Text
}
addDefaultInfo :: DefaultInfo -> OpenApi -> OpenApi
We can set title, description and version for the application. Here is an example:
setSwagger :: Server IO -> Server IO
setSwagger = withSwagger config
where
config =
(def :: SwaggerConfig IO)
{ mapSchema = pure . addDefaultInfo info
}
info =
def
{ title = "Weather forecast"
, description =
"JSON API example for mig library which shows how to forecast weather to authorized users"
, version = "0.1.0"
}
Describe the routes
Often we would like to add some useful documentation on the routes. We can do it with the functions:
-- | Sets description of the route
setDescription :: Text -> Server m -> Server m
-- | Sets summary of the route
setSummary :: Text -> Server m -> Server m
-- | Adds OpenApi tag to the route
addTag :: Text -> Server m -> Server m
We can apply those functions at definition of the route. Also we can describe the inputs for the route:
{-| Appends descriptiton for the inputs. It passes pairs for
@(input-name, input-description)@. Special name request-body
is dedicated to request body input nd raw-input is dedicated
to raw input
-}
describeInputs :: [(Text, Text)] -> Server m -> Server m
It takes a map from input parameter name to description. There is special name "request-body"
for the request body input.
An example:
server = "calculator" /.
mconcat
[ describeAdd $ "add" /. add
, describeMul $ "mul" /. mull
]
where
describeAdd = setDescription "Performs addition" . describeArgs
describeMul = setDescription "Performs multiplication" . describeArgs
describeArgs = describeInputs [("a", "first argument"), ("b", "second argument")]
add, mul :: Capture "a" Int -> Capture "b" Int -> Get (Resp Int)
Summary
In this chapter we have learned how to tweak our swagger servers and make them more user-friendly.
JSON example: weather forecast
We have learned all we need to know about mig
to be able to build something cool with it.
Let's build a weather forecast application. The app has registered users
which can request authorization tokens. With that token users can request for weather
in specific city and on specific time and also they can update the weather data.
For simplicity we omit user registration and defining roles for the users.
Domain for our application
Let's define main types for our application in the module Types.hs
.
We will import Mig.Json.IO
to bring in the scope some classes and types
common for HTTP-servers:
module Types where
import Data.Time (Day)
import Mig.Json.IO
Domain of users
There are users in the application that can register and get session tokens:
data User = User
{ name :: Text
, pass :: Text
}
deriving (Generic, ToJSON, FromJSON, ToSchema)
newtype AuthToken = AuthToken Text
deriving newtype
(ToJSON, FromJSON, FromHttpApiData, Eq, Ord, Show, ToParamSchema, ToSchema)
We need the instances to pass the data over HTTP wires.
Domain of weather
We are going to query weather info by location and date:
newtype DayInterval = DayInterval Int
deriving newtype (ToJSON, FromJSON, FromHttpApiData, ToParamSchema)
data Timed a = Timed
{ from :: Day
, content :: [a]
}
deriving (Generic, ToJSON, FromJSON)
deriving instance (ToSchema a) => ToSchema (Timed a)
newtype Location = Location Text
deriving newtype
(ToJSON, FromJSON, FromHttpApiData, Eq, Ord, Show, ToParamSchema, ToSchema)
The weather has information on temperature, speed of the wind, sun/rain ratio and pressure:
data WeatherData = WeatherData
{ temperature :: Int
, windSpeed :: Int
, sunRainRatio :: Int
, pressure :: Int
}
deriving (Generic, ToJSON, FromJSON, ToSchema)
Also some users can update DB of weather:
-- | Update weather data
data UpdateData = UpdateData
{ day :: Day
, location :: Location
, content :: WeatherData
}
deriving (Generic, ToJSON, FromJSON, ToSchema)
That is our domain for the weather application.
Lets define a server
We are going to build JSON HTTP application. For that we will use the module Mig.Json.IO
which provides handy types specified to our domain.
We expect our application to have shared context Env
which we pass to all handlers.
import Mig.Json.IO
import Types
server :: Env -> Server IO
server env =
withSwagger def $
"api/v1/weather"
/. [ auth
, withAuth env $: app
]
where
auth = "get/auth-token" /. requestAuthToken env
app =
mconcat
[ "get/weather" /. getWeather env
, "update" /. updateWeather env
]
-- | Site internal shared context
data Env = Env
-- weather
getWeather ::
Env ->
Capture "location" Location ->
Capture "day" Day ->
Capture "day-interval" DayInterval ->
Get (RespOr Text (Timed WeatherData))
getWeather = undefined
updateWeather ::
Env ->
Body UpdateData ->
Post (RespOr Text ())
updateWeather = undefined
-- users
requestAuthToken :: Env -> Body User -> Post (RespOr Text AuthToken)
requestAuthToken = undefined
withAuth :: Env -> Header "auth" AuthToken -> Plugin IO
withAuth = undefined
We have one route to query for token:
requestAuthToken :: Env -> Body User -> Post (RespOr Text AuthToken)
And two routes that query info on weather and update it:
getWeather ::
Env ->
Capture "location" Location ->
Capture "day" Day ->
Capture "day-interval" DayInterval ->
Get (RespOr Text (Timed WeatherData))
updateWeather ::
Env ->
Body UpdateData ->
Post (RespOr Text ())
Also we have a plugin that filters out non authorized calls:
withAuth :: Env -> Header "auth" AuthToken -> Plugin IO
From its type-signature we can assume that authorization token is passed in the header of the request.
The structure of the server
We define server as a collection of actions that can be performed. The server is defined in terms of interfaces. We can initialize those interfaces and pass them to handlers.
Our app has several domains:
-
users and sessions
-
weather DB interface
-
process life cycle: logging, startup, cleanup, etc
So the server environment has three parts:
data Env = Env
{ auth :: Auth
, weather :: Weather
, proc :: Proc
}
Let's define operations for those domains. We define them in the module Interface.hs
.
User domain
For the user we can do
-
check that user is valid and can use the app
-
allocate new authorization token
-
check that token is valid
-
expire the token (make it invalid)
-- authorization interface
data Auth = Auth
{ newToken :: User -> IO AuthToken
, validUser :: User -> IO Bool
, validToken :: AuthToken -> IO Bool
, expireToken :: AuthToken -> IO ()
}
Weather domain
For the weather we can query info and update it:
-- weather forecast interface
data Weather = Weather
{ get :: Location -> Day -> DayInterval -> IO (Maybe (Timed WeatherData))
, update :: UpdateData -> IO ()
}
Process domain
For the application process we keep all server life cycle tools which are not related to business logic domain. It can be logging, metrics, startup and cleanup actions:
-- | Process interface
data Proc = Proc
{ startup :: IO ()
, cleanup :: IO ()
, Logger :: Logger
}
-- logger interface
data Logger = Logger
{ info :: LogFun
, debug :: LogFun
, error :: LogFun
}
type LogFun = Value -> IO ()
We log JSON-values. As a helper functions we create functions that can log anything which is convertible to JSON:
logInfo :: (ToJSON a) => Env -> a -> IO ()
logInfo env = env.proc.logger.info . toJSON
logDebug :: (ToJSON a) => Env -> a -> IO ()
logDebug env = env.proc.logger.debug . toJSON
logError :: (ToJSON a) => Env -> a -> IO ()
logError env = env.proc.logger.error . toJSON
Using interfaces
It's interesting to note how all actions on shared state can be expressed as interfaces. We will declare the concrete mutable representation later but for now it is ok to hide them with not implemented yet functions. This also allows us to create mock applications for testing and substitute implementations without changing the code for the server.
Define server in terms of interfaces
As we have defined the main operations of the application we can complete the definition of the server. Let's define the routes for weather domain first as they are more simple.
Weather domain
We can query the weather forecast with function:
getWeather ::
Env ->
Capture "location" Location ->
Capture "day" Day ->
Capture "day-interval" DayInterval ->
Get (RespOr Text (Timed WeatherData))
getWeather env (Capture location) (Capture fromDay) (Capture interval) = Send $ do
logInfo @Text env "get the weather forecast"
mResult <- env.weather.get location fromDay interval
pure $ case mResult of
Just result -> ok result
Nothing -> bad status400 "No data"
We log that call to get weather is in the progress. Then we try to fetch weather data and if it has the data we return it to the user otherwise we report error.
Let's update the weather data:
updateWeather ::
Env ->
Body UpdateData ->
Post (Resp ())
updateWeather env (Body updateData) = Send $ do
logInfo @Text env "update the weather data"
ok <$> env.weather.update updateData
We log the report and update the weather data.
The user domain
Let's give the user access token and check that token is valid.
Let's allocate a new token in the handler requestAuthToken
:
requestAuthToken :: Env -> Body User -> Post (RespOr Text AuthToken)
requestAuthToken env (Body user) = Send $ do
logInfo env ("get new auth token for: " <> user.name)
isValid <- env.auth.validUser user
if isValid
then do
token <- env.auth.newToken user
void $ forkIO $ setExpireTimer token
pure $ ok token
else do
logError env $ Text.unwords ["User", user.name, "does not have access to the service"]
pure $ bad unauthorized401 "User is not valid"
where
setExpireTimer :: AuthToken -> IO ()
setExpireTimer token = do
threadDelay (1_000_000 * 60 * 10) -- 10 minutes
env.auth.expireToken token
We check that user is valid and if the user is valid we give user a token and also set the expiration for it. We will expire it 10 minutes after registration. The expiration is concurrent process that is forked from the thread that handles the request. If user has no rights to use our service we report error.
Let's check for authorization tokens. Ideally we would like to add this action to all handlers of our application. We would like to keep the business logic handlers for the weather domain the same. And we can do it with plugin. Let's define such a plugin that expects authorization tokens with required header:
withAuth :: Env -> Header "auth" AuthToken -> Plugin IO
withAuth env (Header token) = processResponse $ \getResp -> do
isOk <- env.auth.validToken token
if isOk
then getResp
else do
logError env errMessage
pure $ Just (bad status500 $ Text.encodeUtf8 errMessage)
where
errMessage = "Token is invalid"
We have covered in depth how to implement it in the chapter on Plugins so this code should look familiar to us.
Run application
That completes the definition of the server. Let's run it. We define the main function
in the module Main.hs
:
main :: IO ()
main = do
env <- initEnv port
env.proc.startup
runServer port (server env)
`finally` env.proc.cleanup
where
port = 8085
initEnv :: Port -> IO Env
initEnv = undefined
The initialization of interfaces is yet to be defined. So this is all we need to start the server.
Implementation of the interfaces
For the purpose of the example we will create a mock application.
A bit more detailed implementation is in the source code of the mig
library.
See example JsonApi
.
Mock application
We can create a mock application with one user.
import Data.ByteString.Char8 qualified as B
import Data.Yaml qualified as Yaml
import Data.Aeson qualified as Json
initEnv :: Port -> IO Env
initEnv port = pure $ Env initAuth initWeather (initProc proc)
-- | Application with single user john with token that never expires
initAuth :: Auth
initAuth =
Auth
{ newToken = const $ pure AuthToken "john-token"
, validUser = \(User name pass) = pure $ name == "john" && pass == "123"
, validToken = (\AuthToken token) -> pure (token == "john-token")
, expireToken = const $ pure ()
}
initWeather = Weather
{ get = \location day dayInterval -> pure Nothing
, update = \updateData -> pure ()
}
initProc :: Port -> Proc
initProc =
pure Proc
{ logger = logger
, startup = logger.info $ "App started on port: " <> Text.pack (show port)
, cleanup = logger.info "App shutdown"
}
where
logger = initLogger
initLogger :: Logger
initLogger = Logger
{ info = logBy "info"
, debug = logBy "debug"
, error = logBy "error"
}
where
logBy :: Text -> Json.Value msg -> IO ()
logBy level msg = B.putStrLn . Yaml.encode . addLogPrefix $
Json.object [ "level" .= level, "message" .= msg ]
addLogPrefix :: Json.Value -> Json.Value
addLogPrefix val = Json.object ["log" .= val]
We can start the application and try it out with swagger.
Exercises
You can find the complete code of the example in the mig
repo.
-
implement routes for user registration. Only registered users can receive the authorization token.
-
implement roles for the users:
- admin: can manage users
- db-writer: can update weather forecast
- visitor: can only view forecasts
-
implement in-memory storage of the weather DB. Use maps to store weather data and info on valid users and tokens.
-
implement real logger with
fast-logger
library -
implement interface that connects application to some real DB. The code for the server should stay the same and only initialization of interface should change. Use one of the DB libraries for Haskell:
hasql
,postgresql-simple
Summary
In this chapter we have defined a more substantial example of JSON HTTP application and applied various concepts in practice.
HTML example: Blog site
We have seen many examples of JSON applications. But we can send data in other formats too.
Most common format for human-readable information for HTTP applications
is HTML. Any site that we visit sends HTML pages to us. Let's create a simple
site that shows blog posts and quotes and we can add new blog posts to it.
Complete code for example is in the mig
repo
We will cover the most interesting parts of the application.
As in previous example we are going to use IO-based server. The common actions that change internal state of the site or query useful data are expressed as collection of interfaces. All handlers accept interfaces as first argument.
The structure of the application is similar to the weather forecast example. The modules with their purpose:
Types
- types that cover domain of our applicationServer
- defines server API and handlersMain
- runs the serverView
- renders types as HTML-pagesInterface
- actions that can be performed on shared internal state of the appInit
- initialization of the interfacesContent
- some mock data to show on pageInternal
- internal state of the site
Using preset module for HTLM servers
As we define server based on IO
and HTML
we will use the module Mig.Html.IO
.
Note that there is a difference in types from JSON
brother of that module.
We will not see the Resp
type in the signatures of the handlers.
Because for HTML
the type synonyms to Send
fix the output of Send
-type
so that it always has value wrapped in Resp
:
type Get a = Send GET IO (Resp a)
The Resp
-type is built in the type because for HTML we almost never need the RespOr
with different type of errors. Our errors are going to be HTML-pages to as the result.
So we report errors to user in HTML-pages.
Run a server
Let's define a function that will start an empty server:
module Main (
main,
) where
import Mig.Html.IO (runServer)
main :: IO ()
main = do
site <- initSite
runServer port (server site)
where
port = 8085
-- | Placeholder for collection of interfaces
data Site = Site
-- | Initialize site's interfaces
initSite :: IO Site
initSite = pure Site
-- | Create a server that works in terms of interfaces
server :: Site -> Server IO
server _ = mempty
Define server routes
Let's define the server and its routes. For our blog post site we are going to show the pages:
-
main page with list of blog posts and greeting
-
show random blog post
-
show random quote
-
save new blog post
-
list all blog posts
Also our site serves static files which contain CSS-style file and images
We keep them in separate directory called resources
.
Here is the definition of the server:
-- | Server definition. Note how we assemble it from parts with monoid method mconcat.
server :: Site -> Server IO
server site =
logRoutes $
mconcat
[ "blog"
/. [ readServer
, writeServer
]
, defaultPage
, addFavicon $ "static" /. staticFiles resourceFiles
]
The site serves several sub-servers:
readServer
- pages that fetch content for uswriteServer
- pages to update the content. To save new blog postdefaultPage
- main page of the appstaticFiles
withaddFavicon
- serves static files and adds icon for our site that is shown in the tab of the browser
default page server
Let's define the simplest server, the default page:
-- default main page
defaultPage =
mconcat
[ "/" /. handleGreeting site
, "index.html" /. handleGreeting site
]
handleGreeting :: Site -> Get (Page Greeting)
It links two routes to the handleGreeting
handler. We serve two routs: the route
of the site and default route for main page "/index.html".
For now we can think of Page
as main template of our site which contains
some value that is renderable to HTML. Renderer converts the value to HTML
and injects it to the site's template.
Read server
Let's define read-only pages for our site.
-- server to read info.
-- We can read blog posts and quotes.
readServer =
mconcat
[ "read"
/. [ "post" /. handleBlogPost site
, "quote" /. handleQuote site
]
, "list" /. handleListPosts site
]
handleBlogPost :: Site -> Optional "id" BlogPostId -> Get (Page BlogPost)
handleQuote :: Site -> Get (Page Quote)
handleListPosts :: Site -> Get (Page ListPosts)
We have three pages to read in our site:
handleBlogPost
- shows random blog posthandleQuote
- shows random quotehandleListPosts
- shows a list of all posts
Write server
Let's define a route to add new blog posts to the site:
-- server to write new blog posts
writeServer =
"write"
/. [ toServer $ handleWriteForm site
, toServer $ handleWriteSubmit site
]
handleWriteForm :: Site -> Get (Page WritePost)
handleWriteSubmit :: Site -> Body FormUrlEncoded SubmitBlogPost -> Post (Page BlogPost)
We have two different routes which are served on the same path.
The first route shows the page with form to fill the post data.
The second route is triggered when we hit the submit button.
The data in the form is send to the site as request body with Content-Type
set to corresponding FormUrlEncoded
type.
We can serve several route handlers on the same path if they differ by
HTTP-method, input type or output type. In this example we use toServer
function
to convert route handler function to Server IO
.
As we can see all handlers expect the value Site
as first argument.
This value contains interfaces for all actions that can be performed with our site.
Static files
We serve static files with the line:
, addFavicon $ "static" /. staticFiles resourceFiles
Let's explain what happens here. The standard function staticFiles
takes a collection of files and turns them into server:
staticFiels :: MonadIO m => [(FilePath, ByteString)] -> Server m
The list of pairs contains pair of (path-to-file, byte-string-content-of-the-file)
.
It is convenient to use it with function embedRecursiveDir
which
embeds all files from directory to Haskell executable. It comes with library file-embed-lzma
.
We use it like this:
server = "static" /. staticFiles resourceFiles
resourceFiles :: [(FilePath, ByteString)]
resourceFiles = $(embedRecursiveDir "Html/resources")
For our site we use milligram.css
framework for styling and couple of
pictures.
Also let's discuss addFavicon
function:
addFavicon = addPathLink "favicon.ico" "static/lambda-logo.png"
It shows interesting concept of linking to the parts of the server.
The standard function addPathLink
serves all calls to favicon.ico
with handler on path "static/lambda-logo.png"
.
This way we can create additional links to the server.
Also we can reuse the sub-parts of another server in our server with
function atPath
:
{-| Sub-server for a server on given path
it might be usefule to emulate links from one route to another within the server
or reuse part of the server inside another server.
-}
atPath :: forall m. Api.Path -> Server m -> Server m
It creates sub-server for all routes that match given path-prefix.
So it can be not only single route but the whole sub-server.
The function addPathLink
rides on top of that useful function.
Overview of the site
So for our site we have 5 handlers. Let's recall them:
handleBlogPost :: Site -> Optional "id" BlogPostId -> Get (Page BlogPost)
handleQuote :: Site -> Get (Page Quote)
handleListPosts :: Site -> Get (Page ListPosts)
handleWriteForm :: Site -> Get (Page WritePost)
handleWriteSubmit :: Site -> Body FormUrlEncoded SubmitBlogPost -> Post (Page BlogPost)
We can see from type signatures all the types for our domain. Let's define the types.
Domain model of the blog site
We are going to show Blog posts. And we have a form to input a new content to the site. We can define the types for domain as follows:
-- | Web-page for our site
newtype Page a = Page a
-- | Greeting page
data Greeting = Greeting [BlogPost]
-- | Form to submit new post
data WritePost = WritePost
-- | List all posts
newtype ListPosts = ListPosts [BlogPost]
-- | Blog post id
newtype BlogPostId = BlogPostId {unBlogPostId :: UUID}
deriving newtype (FromHttpApiData, ToHttpApiData, Eq, Show, FromJSON, ToParamSchema)
data BlogPostView
= ViewBlogPost BlogPost
-- | error: post not found by id
| PostNotFound BlogPostId
-- | Blog post
data BlogPost = BlogPost
{ id :: BlogPostId
, title :: Text
, createdAt :: UTCTime
, content :: Text
}
-- | A quote
data Quote = Quote
{ content :: Text
}
-- | Data to submit new blog post
data SubmitBlogPost = SubmitBlogPost
{ title :: Text
, content :: Text
}
deriving (Generic, FromForm, ToSchema)
We need to define certain instances to be able to send the data over HTTP wires.
The types come from standard libraries for web development in Haskell: openapi3
, http-api-data
, aeson
.
Interfaces for the site
Our web site is going to support the following actions:
{-| Web site actions. It defines interfaces that connect logic of our site
with outside world: DBs, logger.
-}
data Site = Site
{ readBlogPost :: BlogPostId -> IO (Maybe BlogPost)
, writeBlogPost :: SubmitBlogPost -> IO BlogPostId
, listBlogPosts :: IO [BlogPost]
, readQuote :: IO Quote
, logInfo :: Text -> IO ()
, cleanup :: IO ()
}
The actions are natural and follow the design of domain and server.
Implement handlers
Let's implement the handlers in terms of interfaces.
Default page
Let's greet the user:
-- | Greet the user on main page
handleGreeting :: Site -> Get (Page Greeting)
handleGreeting site =
Send $ ok . Page . Greeting <$> site.listBlogPosts
We get the list of all blog posts and wrap them in Greeting
page.
Read site
Let's read the blog post:
-- | Read blog post by id
handleBlogPost :: Site -> Optional "id" BlogPostId -> Get (Page BlogPostView)
handleBlogPost site (Optional mBlogId) = Send $
case mBlogId of
Nothing -> ok . Page . ViewBlogPost <$> randomBlogPost site
Just blogId ->
maybe
(bad notFound404 $ Page $ PostNotFound blogId)
(ok . Page . ViewBlogPost)
<$> site.readBlogPost blogId
We have an optional query parameter that contains id
of the blog post.
If id
is missing we return some random blog post.
If there is no blog post that corresponds to id we return error with page not found status.
Let's read the random quote:
handleQuote :: Site -> Get (Page Quote)
handleQuote site = Send $ ok . Page <$> site.readQuote
Let's show all blog posts as a menu to the user:
handleListPosts :: Site -> Get (Page ListPosts)
handleListPosts site = Send $ do
ok . Page . ListPosts <$> site.listBlogPosts
Write site
Let's show the form to the user to fill new post data:
handleWriteForm :: Site -> Get (Page WritePost)
handleWriteForm _site =
Send $ pure $ ok $ Page WritePost
As we can see we just return the tag of the page that encodes
the content with form. We will define the HTML-form in the rendering module View.hs
.
Let's define the logic to save the submitted data to application:
-- | Submit form with data provided by the user
handleWriteSubmit :: Site -> Body FormUrlEncoded SubmitBlogPost -> Post (Page BlogPostView)
handleWriteSubmit site (Body submitData) = Send $ do
pid <- site.writeBlogPost submitData
maybe
(bad notFound404 $ Page $ PostNotFound pid)
(ok . Page . ViewBlogPost)
<$> site.readBlogPost pid
In this example we save the new blog post and read that post after saving to show it to the user.
As we can see the handlers follow the interfaces of the site. We just wrap data in the content.
View HTML pages
To render HTML pages we use blaze-html
library. All we need to do is to define
instances for all types that show up in the result of handlers.
The type Page
is a container for our web site main template:
-- writes the template for main page
instance (ToMarkup a) => ToMarkup (Page a) where
toMarkup (Page page) = siteTemplate (H.toMarkup page)
siteTemplate :: Html -> Html
Also we define ToMarkup
instances for all elements in the site:
instance ToMarkup Greeting where
instance ToMarkup WritePost where
instance ToMarkup BlogPostView where
...
You can find the full code in the sources.
Internal state implementation
Our website server is ready to be launched. We only need to define the interfaces. You can find the implementation in the source code for the example. See the modules:
Init
- initializes the interfaceContent
- contains run-time mock dataInternal.State
- mutable state to save blog posts in memory
Summary
We have learned how to build HTML-based servers. It goes almost the same as with JSON applications only we have some twists regarding static files.
HTML goodies
In this chapter we will explore several techniques that make development of the servers that serve HTML a bit more pleasant.
We will study how to:
- use cookies
- create type-safe stable URLs
- use templates to render HTML-code
Cookies
A cookie is a special HTTP-header that asks the browser to save some information in local store so that it can be accessed in the sequence of requests. Often cookies are used to identify the logged in users and keep their sessions without the need to re-login. For great in-depth explanation of the cookies you can read this article.
How to save a cookie in the local storage
We can ask the browser to set the cookie with a function:
setCookie :: (ToForm cookie, IsResp resp) => SetCookie cookie -> resp -> resp
We have a function to initialise basic cookies:
-- | Create a cookie from content with default settings
defCookie :: a -> SetCookie a
The auxiliary parameters let us specify expiration time and other useful
parameters that control cookie life-cycle (see the type SetCookie
for details).
Note that the cookie should be an instance of ToForm
class. It's easy to derive
the instance with generics:
data MyCookie = MyCookie
{ token :: Text
, secretNumber :: Int
} deriving (Generic, ToForm, FromForm)
We need FromForm
instance to read the cookie.
How to read the cookies
The cookie is just an HTTP-Header with special name.
To fetch the cookie on request we have a special input newtype
-wrapper:
newtype Cookie = Cookie (Maybe a)
And we can use it just as any other input (like query parameter or capture) in the argument list of a handler function:
import Data.Text qualified as Text
...
showCookieHandler :: Cookie MyCookie -> Get IO (Resp Html Text)
showCookieHandler (Cookie mValue) =
pure $ ok $ case mValue of
Just value -> cookieToText value
Nothing -> "No cookie is set"
where
cookieToText =
Text.unwords
[ "The cookie is:"
, fromString $ show (value.token, value.secretNumber)
]
Note that value of cookie is optional (wrapped in Maybe
) as
often on the first visit to page no cookie is set.
Type-safe stable URLs
In the previous chapter we have discussed HTML-example of prototype for a blog-post site. And while rendering page data to HTML we typed links to pages as text constants. For example:
menu = do
H.div $ do
H.img H.! HA.src "/static/haskell-logo.png" H.! HA.alt "blog logo" H.! HA.width "100pt" H.! HA.style "margin-bottom: 15pt"
H.ul H.! HA.style "list-style: none" $ do
item "/index.html" "main page"
item "/blog/read/post" "next post"
item "/blog/read/quote" "next quote"
item "/blog/write" "write new post"
item "/blog/list" "list all posts"
item ref name =
H.li $ H.a H.! HA.href ref $ H.text name
This code is fragile because we can change the name of some path in the server definition and forget to update it in the View-functions.
To make it a bit more stable the safe URLs are introduced.
Let's start explanation with most basic type Url
:
-- | Url-template type.
data Url = Url
{ path :: Path
-- ^ relative path
, queries :: [(Text, Text)]
-- ^ queries in the URL
, captures :: Map Text Text
-- ^ map of captures
}
It encodes the typical URL. It has static part and two containers for query and capture parameters.
There is a class ToUrl
that let us derive proper URL correspondence for
a give server definition.
Let's explain it on hello-world server. It has two routes:
-- | The server definition
server :: Server IO
server =
"api/v1" /.
[ "hello" /. hello
, "bye" /. bye
]
-- | The handler definition as a function
hello :: Get (Resp Text)
-- | The handler with a query parameter to ask for the user name
bye :: Query "user" Text -> Get (Resp Text)
This example is for JSON
server but let's pretend that it is an HTML-server
and we would like to generate URL
's from server definition for our handlers.
To do it first we will rewrite the definition a bit. We will place the handlers
to a record and create similar record for URLs. This is not strictly necessary but
it will make our code more structured. Also we will create type synonyms for handler's
type-signatures:
type HelloRoute = Get (Resp Text)
type ByeRoute = Query "user" Text -> Get (Resp Text)
data Routes = Routes
{ hello :: HelloRoute
, bye :: ByeRoute
}
-- | The server definition
server :: Server IO
server routes =
"api/v1" /.
[ "hello" /. routes.hello
, "bye" /. routes.bye
]
Let's define the URLs:
data Urls = Urls
{ hello :: UrlOf HelloRoute
, bye :: UrlOf ByeRoute
}
It resembles the handlers code only we use prefix UrlOf
. This is a type-level
function that knows which URL-creation function corresponds to handler.
For a static route with no arguments it will produce just constant Url
.
But for a route with arguments the result URL also is going to depend on those arguments
in case that input is either Query
, Optional
, QueryFlag
or Capture
.
All those inputs affect the look of the resulting URL.
For example for ByeRoute
we get the type:
Query "user" Text -> Url
Let's link URLs to the server definition:
urls :: Urls
urls = Urls{..}
where
hello
:| bye = toUrl (server undefined)
Here we use extension RecordWildCards
to automatically assign proper fields
by name from the where
expression.
Also one new thing is :|
-operator. It is a suffix synonym for ordinary pair type:
data (:|) a b = a :| b
It let us bind to as many outputs as we like without the need for parens:
where
a :| b :| c :| d = toUrl (server undefined)
But we should be cautious to use so many routes as there are in the server definition.
URL
's are matched against the server definition in the same order as they appear in the
server definition. In this way we get stable names for URL handles.
To use URL in the HTML we can use the function:
renderUrl :: (IsString a) => Url -> a
Which can convert it to any string-like type. It's compatible with blaze-html
and we can use it as an argument for href
attribute in the html link constructor.
We have to be careful on the order of URL
's in the definition and make sure that they match
with the order in the server. If inputs are incompatible a run-time error is produced
on the call to the link.
The great part of it is that query or capture arguments are preserved in the URL constructors. And the proper corresponding URL text will be generated from arguments.
How to use HTML-templates
In the previous examples we wrote HTML view code with blaze-html
DSL.
The HTML construction is a Haskell function in this style. But often it is
desirable to write HTML with the holes in it. So that holes can be substituted
with values at run-time. Those files are called templates. Often templates
are written by Web-designers.
In this section we will study how to use mustache
templates with mig
library.
The mustache is a very simple and popular templating engine. For Haskell we have
a great library stache
that
makes it easy to use mustache templates in the Haskell.
I recommend to read the tutorial on how to use the library.
Overview of the mustache
The main idea of the templates is very simple. The arguments are marked with double curly braces:
Hello {{name}}!
Nice to meet you in the {{place}}.
This template expects a JSON
input to be completed with two fields:
{
"name": "John",
"place": "Garden"
}
Also we can render lists of things with special syntax:
Items:
{{#items}}
* [name](ref)
{{/items}}
It expects a JSON
object:
{
"items":
[ { "name": "foo", "ref": "http://foo.com" }
, { "name": "bar", "ref": "http://bar.com" }
]
}
As we can see it's format-agnostic and can work for any text. For HTML there are special marks that let us prevent escaping of HTML special symbols:
{{{content}}}
Triple curly braces mean that we do not need escaping of special symbols and HTML code is trusted and inlined directly. This type of input is often useful for the template for the main page where we define header, footer, menus and we have a single place for the main content of the page which is inlined as plain HTML. Without tripling of the braces w the HTML code will be rendered as text.
How to load templates
To load templates we can use the normal functions from the stache
library.
For this example we will inline them at compile-time in the code:
import Text.Mustache
import Text.Mustache.Compile.TH qualified as TH
mainTemplate :: Template
mainTemplate = $(TH.compileMustacheFile "HtmlTemplate/templates/main.html")
We need TemplateHaskell
language extension activated for that.
Also we include the directory with templates in our cabal file as extra-source-files:
extra-source-files:
HtmlTemplate/templates/main.html
HtmlTemplate/templates/post.html
HtmlTemplate/templates/postNotFound.html
By the way all the code is taken from example in the mig
repo called
HtmlTemplate
.
After we have loaded the templates we can apply them with JSON
values.
We get the data from the handler and convert it to HTML with templates.
We will make a helper function for that:
import Text.Mustache
import Text.Blaze.Html.Renderer.Text qualified as H
import Text.Blaze.Html5 qualified as H
...
renderMustacheHtml :: (ToJSON a) => Template -> a -> Html
renderMustacheHtml template value =
H.preEscapedLazyText $ renderMustache template (toJSON value)
It applies templates to JSON
-like values. Let's look at the simple example:
-- Rendering of a single quote
instance ToMarkup Quote where
toMarkup quote = renderMustacheHtml templates.quote quote
The template templates.quote
is applied to value quote
of the type:
-- | A quote
data Quote = Quote
{ content :: Text
}
deriving (Generic, ToJSON)
Note the deriving of ToJSON
to make it convertible to JSON
.
Let's look at the template.quote
. It is defined in the file HtmlTemplate/templates/quote.html
:
<div> <h2> Quote of the day: </h2> </div>
<div> <p> {{content}} </p> </div>
So it has one argument content
and exactly that is produced from Quote
value
as JSON
.
Rendering links
Often we would like to render links in the HTML and they have the same structure
with two arguments href
and name
. Also we can use stable URl
's as links
which we have just studied. For that the helper type was created:
data Link = Link
{ href :: Url
, name :: Text
}
deriving (Generic, ToJSON)
It's convenient to use it with mustache templates. We can define template:
<a href="{{href}}">{{name}}</a>
And apply the value of the Link
type to it.
For example let's look at the template that lists all available blog-posts:
<div>
<h2> Posts: </h2>
<ul>
{{#posts}}
<li>
<a href="{{href}}"> {{name}} </a>
</li>
{{/posts}}
</ul>
</div>
It expects a JSON
object:
{
"posts":
[ { "href": "foo", "name": "foo" }
, { "href": "bar", "name": "bar" }
]
}
And for that we have a Haskell type that matches this definition:
-- | List all posts
newtype ListPosts = ListPosts [BlogPostLink]
To render it we only need to add top level object with "post"
-field.
-- | Rendering of all submited posts
instance ToMarkup ListPosts where
toMarkup (ListPosts posts) =
renderMustacheHtml templates.listPosts $ toPostLinks posts
toPostLinks :: [BlogPostLink] -> Json.Value
toPostLinks posts =
Json.object ["posts" Json..= fmap toLink posts]
where
toLink :: BlogPostLink -> Link
toLink post =
Link
{ href = urls.blogPost (Optional $ Just post.blogPostId)
, name = post.title
}
Note how we use the URL constructor as a function.
Other template engines
We are not limited with mustache
for templates. The Haskell has many
great templating libraries which also can be used like shakespeare
or heist and many others.
I've chosen stache
as it ports very widespread and simple solution mustache
to Haskell.
But other template engines can be used in the same way. The mig
library is not tied
to any of those libraries. Although I've tried stache
and highly recommend it.
It's easy to use and versatile.
Summary
We have studied several features that can make HTML-servers more easy to build.
- We have discussed how to work with cookies
- How to create stable type-safe URLs
- How to use template engine
stache
(aka mustache) for our sites and make HTML-pages friendly for WEB-designers.
You can study the source code of the example
HtmlTemplate
in the mig
repo to see how those concepts are used in action.
Clients
We can define HTTP-clients from the same definition as servers. Let's start with hello world example.
The server code is:
module Main (main) where
import Mig
main :: IO ()
main = do
putStrLn $ "Server starts on port: " <> show port
runServer port server
where
port = 8085
server :: Server IO
server = "api/v1/hello" /. hello
hello :: Get IO (Resp Json Text)
hello = Send $ pure $ ok "Hello World!"
To turn that into server we use the class ToClient
:
class ToClient a where
-- | converts to client function
toClient :: Server m -> a
-- | how many routes client has
clientArity :: Int
It can convert the Server
definition to a client.
The client relies on special monad Client
, which we can
run with function:
runClient :: ClientConfig -> Client a -> IO (RespOr AnyMedia BL.ByteString a)
-- | Config to run the clients
data ClientConfig = ClientConfig
{ port :: Int
-- ^ port to connect to
, manager :: Http.Manager
-- ^ HTTP-manager
}
To share code between server and client we should slightly modify our server code and introduce a type synonym for the type of the route:
type Hello m = Get m (Resp Json Text)
server :: Hello m -> Server m
server handler = "api/v1/hello" /. handler
hello :: Hello IO
hello = Send $ pure $ ok "Hello World!"
Our server also becomes generic in it's monad type. If we want to create a server we can apply the handler function to server:
helloServer :: Server IO
helloServer = (server hello)
To define a client we use monad Client
as parameter and get server
definition with toClient
method:
helloClient :: Hello Client
helloClient = server helloClient
We need to provide some argument to the server function and
here we use recursive definition we pass the result back to the argument.
This code is ok because we do not need the implementation of the server
in the toClient
function. We rely solely on the type signature and path
to the handler. Which is specified in the server
function the route handler
is never touched by the execution path. So it is ok to use recursive definition.
We can also pass undefined
.
After that we can call a client function:
import Data.ByteString.Lazy qualified as BL
import Mig
import Mig.Client
import Network.HTTP.Client qualified as Http -- from http-client library
main :: IO ()
main = do
config <- ClientConfig port <$> Http.newManager Http.defaultManagerSettings
print =<< runHello config
where
port = 8085
-- | Make it convenient to use
runHello :: ClientConfig -> IO (Either BL.ByteString Text)
runHello config = getRespOrValue <$> runClient config hello
We use function to get result:
getRespOrValue :: RespOr media BL.ByteString a -> Either BL.ByteString a
Several routes
If we have several routes we can use tuples in the result or special
combinator :|
. Let's create a client for Counter example. Let's recall how server is defined:
{-| Server has two routes:
* get - to querry current state
* put - to add some integer to the state
-}
server :: Server App
server =
"counter"
/. [ "get" /. handleGet
, "put" /. handlePut
]
handleGet :: Get App (Resp Int)
handlePut :: Capture "arg" Int -> Get App (Resp ())
Let's parametrize by the monad type to share the code:
-- | Routes for the server
data Routes m = Routes
{ get :: Get m (Resp Int)
, put :: Capture "args" Int -> Post m (Resp ())
}
server :: Routes m -> Server m
server routes =
"counter"
/. [ "get" /. routes.get
, "put" /. routes.put
]
We can define a server by applying routes:
counterServer :: Server IO
counterServer = server (Routes handleGet handlePut)
To create client we use ToClient
class again:
counterClient :: Routes Client
counterClient = Routes getClient putClient
where
getClient :| putClient = toClient (server counterClient)
We use recursive definition to tie the knot and provide dummy functions to get the server.
The :|
combinator is just a convenient synonym for pair (,)
.
It can be handy if we have arbitrary number of the routes:
routeA
:| routeB
:| routeC
:| routeD
... = toClient
Here we rely on the Haskell ability to pattern match on the constructors. In Haskell we can use not only single variables on the left side but also we can use a constructor. So this is a valid Haskell expression:
ints :: [Int]
strings :: [String]
(ints, strings) = unzip [(1,"a"), (2, "b")]
The same trick is used to specify several routes at once:
where
getClient :| putClient = toClient (server counterClient)
The method toClient
by the output type knows how many routes
to fetch from server definition with the help of clientArity
method.
Also we can write this definition with just a tuple:
where
(getClient, putClient) = toClient (server counterClient)
FromClient
class
Often we do not need the request input wrappers on the level of Http-client. We would like to get the function:
putClient :: Int -> IO (Either ByteString ())
Instead of
putClient :: Capture "arg" Int -> IO (Either ByteString ())
For that we have a class which can strip away all newtype
wrappers:
class FromClient a where
type ClientResult a :: Type
fromClient :: a -> ClientResult a
Associated class ClientResult
can map from wrapped version to unwrapped one.
We can use it simplify client functions after transformation:
-- defined in the library
newtype Client' a = Client' (ReaderT ClientConfig IO a)
type ClientOr a = Client' (Either BL.ByteString a)
-- our client functions:
runGet :: ClientOr Int
runGet = getRespOrValue <$> fromClient getClient
runPut :: Int -> ClientOr ()
runPut = getRespOrValue <$> fromClient putClient
We use special variant of client monad which encapsulates ClientConfig
in the reader.
We can run the client'
with function:
runClient' :: ClientConfig -> Client' a -> IO a
So with FromClass
we can unwrap all newtype
arguments from wrappers
and get ClientConfig
encapsulated in the reader monad.
The structure of the client
So let's recap. To define a client we make server definition generic in the underlying monad and usually introduce a data structure for the routes:
data Routes m = Routes
{ auth :: Auth m
, getBlogPost :: GetBlogPost m
, writeBlogPost :: WriteBlogPost m
}
server :: Routes m -> Server m
server routes = ...
Also it's good to create the type synonyms for routes so that we do not need to retype them twice for servers and clients.
After that we can use ToClient
class to convert server to client:
appClient :: Routes Client
appClient = Routes {..}
auth :| getBlogPost :| writeBlogPost = toClient $ toClient appClient
When we got the client we can simplify definition
by stripping away all newtype
-wrappers:
runAuth :: Arg1 -> Arg2 -> ClientOr AuthId
runAuth arg1 arg2 = getRespOrValue <$> fromClient appClient.auth arg1 arg2
runGetBlogPost :: Arg1 -> ClientOr BlogPost
runGetBlogPost arg1 = getRespOrValue <$> fromClient appClient.getBlogPost arg1
...
Examples of the clients
You can find examples of the clients in the examples
directory of the mig repo.
There are several examples:
HelloClient
- basic hello world clientRouteArgsClient
- client with many routes and all sorts of inputsCounterClient
- how to build client and server from the same code
Reference
Here is the list of main functions, types and classes
How to build server
-- server on path
(/.) :: ToServer a => Path -> a -> Server (MonadOf m)
-- alternative cases for server:
mconcat, (<>)
-- | take sub-server at path
atPath :: Path -> Server m -> Server m
toServer :: ToServer a => a -> Server (MonadOf m)
How to run server
runServer :: Int -> Server IO -> IO ()
runServer' :: ServerConfig -> Int -> Server IO -> IO ()
Server presets
Mig
- generic typesMig.IO
-IO
-based servers with generic return typesMig.Json
- JSON-based serversMig.Html
- HTML-based serversMig.Json.IO
- JSON and IO-based serversMig.Html.IO
- HTML and IO-based servers
Media types
* Json
* Text
* Html
* FormUrlEncoded
* AnyMedia
* OctetStream
Request inputs
-- required query parameter
newtype Body media value = Body value
-- required query parameter
newtype Query name value = Query value
-- optional query parameter
newtype Optional name value = Optional (Maybe value)
-- required header parameter
newtype Header name value = Header value
-- optional header parameter
newtype OptionalHeader name value = OptionalHeader (Maybe value)
-- capture in path parameter
newtype Capture name value = Capture value
-- boolean query flag parameter
newtype QueryFlag name = QueryFlag Bool
-- optional cookies (set in the header)
newtype Cookie value = Cookie (Maybe value)
-- Is connection made over SSL
newtype IsSecure = IsSecure Bool
-- full path with query parameters
newtype PathInfo = Path [Text]
-- | low-level request
newtype RawRequest = RawRequest Request
Request outputs
-- | generic route handler
newtype Send method m a = Send (m a)
-- Sepcific methos
type Get m a = Send GET m a
type Post m a = Send POST m a
...
-- Response where type of the value and error are the same
-- or only succesful result is expected
data Resp media a = Resp ...
-- Response where error and result have different types but media type is the same
data RespOr media err a = RespOr ...
The response type class:
class IsResp a where
-- | the type of response body value
type RespBody a :: Type
-- | the type of an error
type RespError a :: Type
-- | Returns valid repsonse with 200 status
ok :: RespBody a -> a
-- | Returns an error with given status
bad :: Status -> RespError a -> a
-- | response with no content
noContent :: Status -> a
-- | Add some header to the response
addHeaders :: ResponseHeaders -> a -> a
-- | Sets repsonse status
setStatus :: Status -> a -> a
-- | Set the media type of the response
setMedia :: MediaType -> a -> a
-- | Reads the media type by response type
getMedia :: MediaType
-- | Converts value to low-level response
toResponse :: a -> Response
setHeader :: (IsResp a, ToHttpApiData h) => HeaderName -> h -> a -> a
-- | Set cookie as http header from form url encoded value
setCookie :: (ToForm cookie, IsResp resp) => SetCookie cookie -> resp -> resp
data SetCookie a = SetCookie
{ cookie :: a
, expires :: Maybe UTCTime
, domain :: Maybe Text
, path :: Maybe Text
, secure :: Bool
, httpOnly :: Bool
}
-- | Default cookie which sets only the cookie itself.
defCookie :: a -> SetCookie a
-- | Bad request. The @bad@ response with 400 status.
badReq :: (IsResp a) => RespError a -> a
-- | Internal server error. The @bad@ response with 500 status.
internalServerError :: (IsResp a) => RespError a -> a
-- | Not implemented route. The @bad@ response with 501 status.
notImplemented :: (IsResp a) => RespError a -> a
-- | Redirect to url. It is @bad@ response with 302 status
-- and set header of "Location" to a given URL.
redirect :: (IsResp a) => Text -> a
Plugins
applyPlugin, ($:) :: ToPlugin a =>
a -> Server (MonadOf a) -> Server (MonadOf a)
-- composition of plugins:
Monoid(..): mconcat, (<>), mempty
specific servers
-- | add wagger to server
withSwagger :: SwaggerConfig m -> Server m -> Server m
-- | add link from one route to another
addPathLink :: Path -> Path -> Server m
-- static files
staticFiles :: [(FilePath, ByteString)] -> Server m
specific plugins
-- prepend or append some acction to all routes
prependServerAction, appendServerAction :: MonadIO m => m () -> Plugin m
-- change the response
processResponse :: (m (Maybe Response) -> m (Maybe Response)) -> Plugin m
-- only secure routes are allowed
whenSecure :: forall m. (MonadIO m) => Plugin m
-- logging with putStrLn for debug traces
logHttp :: Verbosity -> Plugin m
-- logging with custom logger
logHttpBy :: (Json.Value -> m ()) -> Verbosity -> Plugin m
-- | simple authorization
withHeaderAuth :: WithHeaderAuth -> Plugin m
How to use Reader
-- Derive instance of HasServer class for your Reader-IO based application:
newtype App a = App (ReaderT Env IO a)
deriving newtype (Functor, Applicative, Monad, MonadReader Env, MonadIO, HasServer)
renderServer :: Server App -> Env -> IO (Server IO)
OpenApi and Swagger
-- | Get OpenApi
toOpenApi :: Server m -> OpenApi
-- add swagger to server
withSwagger :: SwaggerConfig m -> Server m -> Server m
-- create swagger server
swagger :: SwaggerConfig m -> m OpenApi -> Server m
-- | Print OpenApi schema
printOpenApi :: Server m -> IO ()
-- | Writes openapi schema to file
writeOpenApi :: FilePath -> Server m -> IO ()
The Swagger config:
-- | Swagger config
data SwaggerConfig m = SwaggerConfig
{ staticDir :: Path
-- ^ path to server swagger (default is "/swagger-ui")
, swaggerFile :: Path
-- ^ swagger file name (default is "swaggger.json")
, mapSchema :: OpenApi -> m OpenApi
-- ^ apply transformation to OpenApi schema on serving OpenApi schema.
-- it is useful to add additional info or set current date in the examples
-- or apply any real-time transformation.
}
instance (Applicative m) => Default (SwaggerConfig m) where
def =
SwaggerConfig
{ staticDir = "swagger-ui"
, swaggerFile = "swagger.json"
, mapSchema = pure
}
Set swagger title and description:
-- | Default info that is often added to OpenApi schema
data DefaultInfo = DefaultInfo
{ title :: Text
, description :: Text
, version :: Text
}
-- adds default info, use it in the mapSwagger field of SwaggerConfig record
addDefaultInfo :: DefaultInfo -> OpenApi -> OpenApi
Describe routes with swagger:
-- | Sets description of the route
setDescription :: Text -> Server m -> Server m
-- | Sets summary of the route
setSummary :: Text -> Server m -> Server m
-- | Adds OpenApi tag to the route
addTag :: Text -> Server m -> Server m
{-| Appends descriptiton for the inputs. It passes pairs for @(input-name, input-description)@.
special name request-body is dedicated to request body input
nd raw-input is dedicated to raw input
-}
describeInputs :: [(Text, Text)] -> Server m -> Server m
Clients
The class to convert server definitions to clients:
class (MapRequest a) => ToClient a where
-- | converts to client function
toClient :: Server m -> a
-- | how many routes client has
clientArity :: Int
An example:
helloWorld :: Get Client (Resp Text)
handleSucc :: Header "Trace-Id" TraceId -> Query "value" Int -> Get Client (Resp Int)
handleSuccOpt :: Optional "value" Int -> Get Client (RespOr Text Int)
helloWorld
:| handleSucc
:| handleSuccOpt = toClient server
We use synonym for pair :|
to avoid redundant parens.
The Client-monad:
newtype Client a = ...
runClient :: ClientConfig -> Client a -> IO (RespOr AnyMedia BL.ByteString a)
-- | Config to run the clients
data ClientConfig = ClientConfig
{ port :: Int
-- ^ port to connect to
, manager :: Http.Manager
-- ^ HTTP-manager
}
The class to strip away request input newtype
wrappers:
class FromClient a where
type ClientResult a :: Type
fromClient :: a -> ClientResult a
It turns types like:
Query "a" Int -> Capture "b" Text -> Get Client (Resp Json Text)
To types:
Int -> Text -> Client' (RespOr Json BL.ByteString Text)
Where Client'
is a monad which encapsulates the ClientConfig
as reader:
newtype Client' a = Client' (ReaderT ClientConfig IO a)
Also we can use the function:
getRespOrValue :: RespOr media BL.ByteString a -> Either BL.ByteString a
To unwrap Resp
from response.
Other utilities
Type-safe URLs
The type-level function UrlOf
creates a type-safe Url
for a given route handler type.
With class ToUrl
we can generate the URLs for a collection of handlers.
class ToUrl a where
toUrl :: Server m -> a
mapUrl :: (Url -> Url) -> a -> a
urlArity :: Int
An example of usage. URL's should be listed in the same order as they appear in the server
urls :: Urls
urls = Urls{..}
where
greeting
:| blogPost
:| listPosts
toUrl (server undefined)
We can render Url
to String
-like type with function:
renderUrl :: IsString a => Url -> a
deriving helpers
Sometimes we need to derive too many types at once to use the type in the library. There are helpers to reduce deriving boiler-plate:
deriveParam ''FooType -- derives parameter instances for a FooType
deriveNewtypeParam ''FooType -- derives parameter instances for a newtype FooType
deriveBody ''FooType -- derives request body instances for a FooType
deriveNewtypeBody ''FooType -- derives request body instances for a newtype FooType
deriveHttp ''FooType -- derives both parameter and request body instances for a FooType
deriveNewtypeHttp ''FooType -- derives both parameterrequest body instances for a newtype FooType
mapDerive fun [''Foo, ''Bar] -- maps deriving over several types
We need to activate TemplateHaskell
, StandaloneDeriving
, DerivingStrategies
, DeriveGeneric
extensions to use it.
Also note that for this to work all types should be in scope. So it better to define drivings at the bottom of the module which is dedicated to types.
Also type should not have generic arguments for deriving to work. If it does we have to declare the types manually. For example:
data Timed a = Timed
{ from :: Day
, content :: [a]
}
deriving (Generic, ToJSON, FromJSON)
deriving instance (ToSchema a) => ToSchema (Timed a)
The data type Timed
has an argument and we have to define the instance
explicitly.
HTML Links
For usage with template engines that expect JSON as argument for template
there is the type Link
in the module Mig.Extra.Html
(also re-exported by all Html
-related modules):
data Link = Link
{ href :: Url
, name :: Text
}
deriving (Generic, ToJSON)
Also it has ToMarkup
instance and rendered as a
-element link.
How to contribute
If you are interested in contribution to library that's great. We have a list of upcoming features in the list of Issues.
Also there is a CI which checks for:
- build and test of all libraries with stack
- build and test of all examples in the directory
examples
with stack fourmolu
formatting- deploy of docs/tutorial to github pages (on push to main branch)
how to automate formatter check
To check for formatting I recommend to use this pre-commit hook:
#!/bin/bash
if command -v fourmolu &> /dev/null
then
files=$(git diff --staged --name-only -- '*.hs')
for file in $files
do
fourmolu -i $file
git add $file
done
else
echo "fourmolu cannot be found"
echo "install fourmolu via cabal install"
fi
Save this as file .git/hooks/pre-commit
in your repo
and make it executable:
chmod +x .git/hooks/pre-commit
The script requires fourmolu
executable which both can be installed from Hackage.
After that all files that you modify will be formatted properly.
Formatting settings are in the file fourmolu.yaml
.