|
| 1 | +--- |
| 2 | +layout: docs |
| 3 | +title: OpenAPI / REST services |
| 4 | +permalink: openapi/ |
| 5 | +--- |
| 6 | + |
| 7 | +# OpenAPI / REST services |
| 8 | + |
| 9 | +In order to expose a Mu server using a OpenAPI or REST interface, we make use of the awesome [Servant](https://docs.servant.dev/en/stable/) library. Both libraries describe the API of a server at the type level, use the notion of _handlers_, and follow a similar structure. |
| 10 | + |
| 11 | +The `mu-servant-server` package contains a function `servantServerHandlers` which unpacks the Mu handlers and repackages them as Servant handlers, with some minor changes to support streaming. The trickiest part, however, is translating the Mu server _type_ into a Servant server _type_. |
| 12 | + |
| 13 | +## Annotating the server |
| 14 | + |
| 15 | +When Mu methods are converted to Servant APIs, you may customize certain aspects of the resulting API, including the route, HTTP method, and HTTP status. Additionally, you must specify which content types use be used when encoding and decoding each type in your schema that appears in your methods. All of this customization is done with annotations, via the `AnnotatedSchema` and `AnnotatedPackage` type families. |
| 16 | + |
| 17 | +For the server we have developed in the [generic RPC section]({% link docs/rpc.md %}), the instances for the services look as follows: |
| 18 | + |
| 19 | +```haskell |
| 20 | +type instance AnnotatedPackage ServantRoute QuickstartService |
| 21 | + = '[ 'AnnService "Greeter" ('ServantTopLevelRoute '["greet"]) |
| 22 | + , 'AnnMethod "Greeter" "SayHello" |
| 23 | + ('ServantRoute '["say", "hello"] 'POST 200), |
| 24 | + ] |
| 25 | +``` |
| 26 | + |
| 27 | +The first annotation defines that the whole service lives in the `/greet` route. Each method then gets its own route and HTTP verb. To execute `SayHello`, one has to make a `POST` request to `/greet/say/hello`. The last element is the HTTP status code to be returned by default, in this case `200` which means success. |
| 28 | + |
| 29 | +You also need to define how message types can be serialized in the API. This will be translated to a `ReqBody` in the corresponding Servant API, which requires a list of acceptable content types for the request. We provide a `DefaultServantContentTypes` which uses JSON for both unary and streaming calls. |
| 30 | + |
| 31 | +```haskell |
| 32 | +type instance |
| 33 | + AnnotatedSchema ServantContentTypes QuickstartSchema = |
| 34 | + '[ 'AnnType "HelloRequest" DefaultServantContentTypes, |
| 35 | + 'AnnType "HelloResponse" DefaultServantContentTypes |
| 36 | + ] |
| 37 | +``` |
| 38 | + |
| 39 | +The `MimeRender`/`MimeUnrender` instances necessary to perform this encoding/decoding must exist for the Haskell type you use to represent messages. In this case, that means that both types must support conversion to JSON, which can be achieved using `mu-schema` in combination with `DerivingVia`. |
| 40 | + |
| 41 | +```haskell |
| 42 | +{-# language DerivingVia #-} |
| 43 | + |
| 44 | +import qualified Data.Aeson as J |
| 45 | +import Mu.Adapter.Json () |
| 46 | + |
| 47 | +newtype HelloRequest = HelloRequest { name :: T.Text } |
| 48 | + deriving ( Show, Eq, Generic |
| 49 | + , ToSchema QuickstartSchema "HelloRequest" |
| 50 | + , FromSchema QuickstartSchema "HelloRequest" ) |
| 51 | + deriving (J.ToJSON, J.FromJSON) |
| 52 | + via (WithSchema QuickstartSchema "HelloRequest" HelloRequest) |
| 53 | +``` |
| 54 | + |
| 55 | + |
| 56 | +If you forget to provide one of these required instances, you will see a message like the following: |
| 57 | + |
| 58 | +``` |
| 59 | + • Missing required AnnotatedPackage ServantRoute type instance |
| 60 | + for "myschema" package |
| 61 | + • When checking the inferred type |
| 62 | +``` |
| 63 | +
|
| 64 | +followed by a large and difficult to read type representing several stuck type families. This message is an indication that you must provide an `AnnotatedPackage` type instance, with a domain of `ServantRoute` for the package with the name `myschema`. |
| 65 | +
|
| 66 | +## Exposing the server |
| 67 | +
|
| 68 | +You are now ready to expose your server using Servant! |
| 69 | +
|
| 70 | +```haskell |
| 71 | +import Mu.Servant.Server |
| 72 | +import Servant.Server |
| 73 | +
|
| 74 | +main = |
| 75 | + let api = packageAPI (quickstartServer @ServerErrorIO) |
| 76 | + server = servantServerHandlers toHandler quickstartServer |
| 77 | + in run 8081 (serve api server) |
| 78 | +``` |
| 79 | + |
| 80 | +The last line uses functions from Servant and Warp to run the server. The `serve` function has two parameters: |
| 81 | +- One is the definition of the API, which can be obtained using the provided `packageAPI` with your server. In this case we had to make explicit the monad we are operating to avoid an ambiguity error. |
| 82 | +- The other is the set of Servant handlers, which can be obtained by using `servantServerHandlers toHandler`. |
| 83 | + |
| 84 | +## Integration with Swagger UI |
| 85 | + |
| 86 | +You can easily expose not only the server itself, but also its [Swagger / OpenAPI](https://swagger.io/) schema easily, alongside a [Swagger UI](https://swagger.io/tools/swagger-ui/) for testing purposes. Here we make use of the awesome [`servant-swagger-ui` package](https://github.com/haskell-servant/servant-swagger-ui). |
| 87 | + |
| 88 | +First of all, you need to specify that you want an additional component in your Servant API. You do so in the annotation: |
| 89 | + |
| 90 | +```haskell |
| 91 | +type instance AnnotatedPackage ServantRoute QuickstartService |
| 92 | + = '[ 'AnnPackage ('ServantAdditional (SwaggerSchemaUI "swagger-ui" "swagger.json")) |
| 93 | + , {- rest of annotations -} ] |
| 94 | +``` |
| 95 | + |
| 96 | +The implementation of this additional component is given by using `servantServerHandlersExtra`, instead of its "non-extra" version. The aforementioned package is ready for consumption in that way: |
| 97 | + |
| 98 | +```haskell |
| 99 | +import Mu.Servant.Server |
| 100 | +import Servant.Server |
| 101 | +import Servant.Swagger.UI |
| 102 | + |
| 103 | +main = |
| 104 | + let svc = quickstartServer @ServerErrorIO |
| 105 | + api = packageAPI svc |
| 106 | + server = servantServerHandlersExtra |
| 107 | + (swaggerSchemaUIServer (swagger svc)) |
| 108 | + toHandler svc |
| 109 | + in run 8081 (serve api server) |
| 110 | +``` |
| 111 | + |
| 112 | +And that's all! When you users surf to `yourserver/swagger-ui` they'll see a color- and featureful explanation of the endpoints of your server. |
| 113 | + |
| 114 | +## Type translation |
| 115 | + |
| 116 | +> This is not required for using `mu-servant-server`, but may help you understanding how it works under the hood and diagnosing problems. |
| 117 | +
|
| 118 | +There are essentially four categories of `Method` types and each of these is translated slightly differently. |
| 119 | + |
| 120 | +### Full unary |
| 121 | + |
| 122 | +Full unary methods have non-streaming arguments and a non-streaming response. Most HTTP endpoints expect unary requests and return unary responses. Unary method handlers look like this |
| 123 | + |
| 124 | +```haskell |
| 125 | +(MonadServer m) => requestType -> m responseType |
| 126 | +``` |
| 127 | + |
| 128 | +For a handler like this, the corresponding "Servant" API type would be |
| 129 | + |
| 130 | +```haskell |
| 131 | +type MyUnaryAPI = |
| 132 | + route :> |
| 133 | + ReqBody ctypes1 requestType :> |
| 134 | + Verb method status ctypes2 responseType |
| 135 | +``` |
| 136 | + |
| 137 | +As you can see, the request body contains a `requestType` value, and the response body contains a `responseType` value. All other types are derived from Mu annotations. |
| 138 | + |
| 139 | +### Server streaming |
| 140 | + |
| 141 | +Server streaming methods have non-streaming arguments, but the response is streamed back to the client. Server stream handlers look like this |
| 142 | + |
| 143 | +```haskell |
| 144 | +(MonadServer m) => requestType -> ConduitT responseType Void m () -> m () |
| 145 | +``` |
| 146 | + |
| 147 | +For a handler like this, the corresponding Servant API type would be |
| 148 | + |
| 149 | +```haskell |
| 150 | +type MyServerStreamAPI = |
| 151 | + route :> |
| 152 | + ReqBody ctypes requestType :> |
| 153 | + Stream method status framing ctype (SourceIO (StreamResult responseType)) |
| 154 | +``` |
| 155 | + |
| 156 | +The request body contains a `requestType` value. The response body is a stream of `StreamResult` responseType@ values. `StreamResult responseType` contains either a `responseType` value or an error message describing a problem that occurred while producing `responseType` values. All other types are derived from Mu annotations. |
| 157 | + |
| 158 | +### Client streaming |
| 159 | + |
| 160 | +Client streaming methods have a streaming argument, but the response is unary. Client stream handlers look like this |
| 161 | + |
| 162 | +```haskell |
| 163 | +(MonadServer m) => ConduitT () requestType m () -> m responseType |
| 164 | +``` |
| 165 | + |
| 166 | +For a handler like this, the corresponding Servant API type would be |
| 167 | + |
| 168 | +```haskell |
| 169 | +type MyClientStreamAPI = |
| 170 | + route :> |
| 171 | + StreamBody framing ctype (SourceIO requestType) :> |
| 172 | + Verb method status ctypes responseType |
| 173 | +``` |
| 174 | + |
| 175 | +### Bidirectional streaming |
| 176 | + |
| 177 | +Bidirectional streaming method have a streaming argument and a streaming response. Bidirectional stream handlers look like this |
| 178 | + |
| 179 | +```haskell |
| 180 | +> (MonadServer m) => ConduitT () requestType m () -> ConduitT responseType Void m () -> m() |
| 181 | +``` |
| 182 | + |
| 183 | +For a handler like this, the corresponding Servant API type would be |
| 184 | + |
| 185 | +```haskell |
| 186 | +type MyBidirectionalStreamAPI = |
| 187 | + StreamBody framing1 ctype1 (SourceIO requestType) :> |
| 188 | + Stream method status framing2 ctype2 (SourceIO (StreamResult responseType)) |
| 189 | +``` |
| 190 | + |
| 191 | +This type should look familiar if you already looked at the server streaming and client streaming examples. The request body is a stream of `requestType` values, and the response body is a stream of `StreamResult responseType` values. All the other types involved are derived from Mu annotations. |
| 192 | + |
0 commit comments