mirror of
https://github.com/higherkindness/mu-haskell.git
synced 2024-10-26 15:49:00 +03:00
Write small intro to GraphQL 🔥 (#185)
This commit is contained in:
parent
94904175b1
commit
68529fdc8e
@ -6,3 +6,171 @@ permalink: intro-graphql/
|
||||
|
||||
# Introduction to Mu-Haskell for GraphQL
|
||||
|
||||
This document will help you get started in writing your first GraphQL server using Mu-Haskell and `mu-graphql`!
|
||||
|
||||
## Using the Stack template
|
||||
|
||||
First of all, we've prepared a Stack template to boostrap your project easily, just run:
|
||||
|
||||
```sh
|
||||
$ stack new your-project https://raw.githubusercontent.com/higherkindness/mu-haskell/master/templates/graphql-server.hsfiles -p "author-email:haskell.curry@47deg.com" -p "author-name:Haskell Curry"
|
||||
```
|
||||
|
||||
After doing this you'll have a compiling project with all the dependencies in place and a simple `Main.hs` file created.
|
||||
|
||||
## Defining your GraphQL Schema
|
||||
|
||||
The template will generate a `schema.graphql` file with a basic "hello" query, feel free to change it with your domain types. For the sake of this intro we'll use the library example from the [Apollo GraphQL docs](https://www.apollographql.com/docs/apollo-server/schema/schema/):
|
||||
|
||||
```graphql
|
||||
type Book {
|
||||
title: String!
|
||||
author: Author!
|
||||
}
|
||||
|
||||
type Author {
|
||||
name: String!
|
||||
books: [Book!]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
authors: [Author!]!
|
||||
books: [Book!]!
|
||||
}
|
||||
```
|
||||
|
||||
This schema defines **three types: a book, an author, and a query**. All GraphQL APIs have a query type which defines all the possible queries you can ask. In our case, we have a query asking for all the authors in the library and another one for all the books. 📚 The exclamation marks all over the place (`!`) in GraphQL mean that the data is required to be returned, removing those would result in a `Maybe type` in Haskell! 😉
|
||||
|
||||
## Implementing the GraphQL Server
|
||||
|
||||
Now let's have a look at the already finished `src/Main.hs` and we'll go bit by bit explaining what is going on:
|
||||
|
||||
```haskell
|
||||
graphql "ServiceDefinition" "schema.graphql"
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
putStrLn "starting GraphQL server on port 8080"
|
||||
runGraphQLAppQuery 8080 server (Proxy @"Query")
|
||||
|
||||
type ServiceMapping = '[
|
||||
"Book" ':-> (Text, Text)
|
||||
, "Author" ':-> Text
|
||||
]
|
||||
|
||||
library :: [(Text, [Text])]
|
||||
library
|
||||
= [ ("Robert Louis Stevenson", ["Treasure Island", "Strange Case of Dr Jekyll and Mr Hyde"])
|
||||
, ("Immanuel Kant", ["Critique of Pure Reason"])
|
||||
, ("Michael Ende", ["The Neverending Story", "Momo"])
|
||||
]
|
||||
|
||||
server :: forall m. MonadServer m => ServerT ServiceMapping ServiceDefinition m _
|
||||
server = resolver
|
||||
( object @"Book"
|
||||
( field @"title" bookTitle
|
||||
, field @"author" bookAuthor )
|
||||
, object @"Author"
|
||||
( field @"name" authorName
|
||||
, field @"books" authorBooks )
|
||||
, object @"Query"
|
||||
( method @"authors" allAuthors
|
||||
, method @"books" allBooks )
|
||||
)
|
||||
where
|
||||
bookTitle :: (Text, Text) -> m Text
|
||||
bookTitle (_, title) = pure title
|
||||
bookAuthor :: (Text, Text) -> m Text
|
||||
bookAuthor (auth, _) = pure auth
|
||||
|
||||
authorName :: Text -> m Text
|
||||
authorName = pure
|
||||
authorBooks :: Text -> m [(Text, Text)]
|
||||
authorBooks name = pure $ (name,) <$> maybe [] snd (find ((==name) . fst) library)
|
||||
|
||||
allAuthors :: m [Text]
|
||||
allAuthors = pure $ fst <$> library
|
||||
allBooks :: m [(Text, Text)]
|
||||
allBooks = pure [(author, title) | (author, books) <- library, title <- books]
|
||||
```
|
||||
|
||||
## Schema generation explained
|
||||
|
||||
Notice this important line:
|
||||
|
||||
```haskell
|
||||
graphql "ServiceDefinition" "schema.graphql"
|
||||
```
|
||||
|
||||
This is where your `schema.graphql` file gets converted to a type-level expression that represents your Schema in terms of Mu-Haskell. Here, we're assigning the new schema the name `ServiceDefinition`, which will be later use in the next section below.
|
||||
|
||||
## Server declaration explained
|
||||
|
||||
As you've seen, we declare the GraphQL server the following way:
|
||||
|
||||
```haskell
|
||||
server :: forall m. MonadServer m => ServerT ServiceMapping ServiceDefinition m _
|
||||
server = resolver
|
||||
( object @"Book"
|
||||
( field @"title" bookTitle
|
||||
, field @"author" bookAuthor )
|
||||
, object @"Author"
|
||||
( field @"name" authorName
|
||||
, field @"books" authorBooks )
|
||||
, object @"Query"
|
||||
( method @"authors" allAuthors
|
||||
, method @"books" allBooks )
|
||||
)
|
||||
```
|
||||
|
||||
Here we use the given name `ServiceMapping` for the type-level generated GraphQL Schema that we've created for you!
|
||||
|
||||
To declare the server, you need provide a resolver for each field, using the `object` and `field` functions. Use `method` for the fields in "Query", since it's a special case for us (you can read more about [why here](https://higherkindness.io/mu-haskell/graphql/#implementing-the-server)).
|
||||
|
||||
> The ordering here is casual, you can declare each type wherever you like (as long as it is inside of the same tuple, of course) and it will still work! 🎉
|
||||
|
||||
## `ServiceMapping` explained
|
||||
|
||||
You might have noticed this strange piece of code:
|
||||
|
||||
```haskell
|
||||
type ServiceMapping = '[
|
||||
"Book" ':-> (Text, Text)
|
||||
, "Author" ':-> Text
|
||||
]
|
||||
```
|
||||
|
||||
This is where we establish the _relationship_ between the types. You may have notice that the GraphQL schema contained **recursive** references for the types, that is OK both for GraphQL and for Haskell, but it would not be the case for _other languages!_
|
||||
|
||||
## Resolvers explained
|
||||
|
||||
Now let's get to the meat. Although GraphQL will use the least amount of calls possible to find the result to your query, you need to provide "paths" or roads to every possible piece of your data. Although the example here is a hardcoded list, it could be anything ranging from a text file to a fully-fledged database! 🚀
|
||||
|
||||
```haskell
|
||||
bookTitle :: (Text, Text) -> m Text
|
||||
bookTitle (_, title) = pure title
|
||||
bookAuthor :: (Text, Text) -> m Text
|
||||
bookAuthor (auth, _) = pure auth
|
||||
|
||||
authorName :: Text -> m Text
|
||||
authorName = pure
|
||||
authorBooks :: Text -> m [(Text, Text)]
|
||||
authorBooks name = pure $ (name,) <$> maybe [] snd (find ((==name) . fst) library)
|
||||
|
||||
allAuthors :: m [Text]
|
||||
allAuthors = pure $ fst <$> library
|
||||
allBooks :: m [(Text, Text)]
|
||||
allBooks = pure [(author, title) | (author, books) <- library, title <- books]
|
||||
```
|
||||
|
||||
What is that `m` that appears everywhere? Well, since we are using `ScopedTypeVariables`, that `m` refers to the above typeclass constrain `MonadServer m`. That is also why we need to lift with `pure` all of our results into the `MonadServer`.
|
||||
|
||||
We tend to put all those resolver functions in a `where` block, but of course you are free to move them around wherever you want! 😜
|
||||
|
||||
## Where to go from here
|
||||
|
||||
Here's a [more complete example](https://github.com/higherkindness/mu-haskell/tree/master/graphql/exe) of how a finished server looks like.
|
||||
|
||||
If you are confused or you'd like to know more about how this all works, have a look at our [GraphQL docs](https://higherkindness.io/mu-haskell/graphql/), or feel free to [open an issue](https://github.com/higherkindness/mu-haskell/issues) in the repo and we'll be happy to help!
|
||||
|
||||
Happy hacking! 🔥
|
||||
|
Loading…
Reference in New Issue
Block a user