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.