A purely functional full-stack web app

A purely functional full-stack web app

Imagine that you had a way to keep your backend code in sync with your frontend code, both generating code (at the type-level even!) and getting compile time guarantees that both sides are using the same version of your schemas. Wouldn’t that be awesome? 😎

This is exactly what an example full-stack web app we’ve created at 47 Degrees accomplishes! 🙌🏼 This is the dream stack we’ve used: Haskell with mu-haskell and mu-graphql for the server, and Elm with elm-graphql for the client. You can follow along or checkout the full code here!

Here is the running library app in the browser!

app

Our source of truth: GraphQL Schemas

We love every kind of book 📚. That’s why we decided some time ago to create a GraphQL library example to feature all the awesomeness we wanted to talk about in this blogpost.

For the communication, we are going to use GraphQL rather than the most commonly used REST. GraphQL has many advantages; in our case, the main one is that, by writing a GraphQL schema, we can use it as the source of truth for our backend and frontend code. Here are the types for our library.graphql definition:

type Book {
  id: Int!
  title: String!
  imageUrl: String!
  author: Author!
}

type Author {
  id: Int!
  name: String!
  books: [Book!]!
}

input NewAuthor {
  name: String!
}

input NewBook {
  title: String!
  authorId: Int!
  imageUrl: String!
}

Did you notice all those exclamation marks? ! means “mandatory” in GraphQL; otherwise, fields are considered optional. This means, for example, that our server code will generate Maybe types in Haskell for them, but we don’t want to complicate the example for now.

The input types are types that describe data that is going to be sent from the client in this case to add new authors and books to the database.

type Query {
  authors(name: String! = "%"): [Author!]!
  books(title: String! = "%"): [Book!]!
}

type Mutation {
  newAuthor(author: NewAuthor!): Author!
  newBook(book: NewBook!): Book!
}

type Subscription {
  allBooks: Book!
}

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

As you can see, even in a fun example project, we are going to use a lot of GraphQL features, including: Queries, Mutations, and . . . 🥁 Subscriptions!

If you are familiar with HTTP, Query is going to be your typical GET endpoint, but for ALL the possible types in our app (as GraphQL generally exposes a single endpoint for everything 💖). Mutation corresponds with PUT, POST, and DELETE. And Subscription is typically used for real-time communication, in our case, implemented via WebSockets.

Server Side → Haskell

Because this is just an example project, does that mean we won’t be using a database or any kind of tracing whatsoever? Wrong! 🙅🏼‍♂️ We are going to use the amazing persistent to provide us with the database and for tracing one of the industry-standard metrics libraries, Promethus (via the brand new mu-prometheus 💪🏼).

module Schema where

-- imports skipped...

graphql "Library" "library.graphql" -- all the magic here! 🪄🎩

-- more code skipped...

Basically, all the magic happens with that line. Here, we are generating type-level representations of our GraphQL Schemas to help build our type-safe backend code!

-- ... following the above code...

share
  [mkPersist sqlSettings, mkMigrate "migrateAll"]
  [persistLowerCase|
Author json
  name T.Text
  UniqueName name
  deriving Show Generic
Book json
  title T.Text
  imageUrl T.Text
  author AuthorId
  UniqueTitlePerAuthor title author
  deriving Show Generic
|]

After that, we have pretty generic persistent code that describes how these entities are mapped to the database and to Haskell types. In some sense, persistent is an ORM for Haskell. The weird [| |] syntax comes from TemplateHaskell, the “macro” or “meta programming” facility in Haskell, which is able to generate those types automagically. 🪄

As mentioned, you can check the full code in our example repo, but here is the main gist of how you would implement such a GraphQL server by using mu-haskell and mu-graphql:

-- language extensions and imports omitted!

main :: IO ()
main = do
  p <- initPrometheus "library"
  runStderrLoggingT $
    withSqliteConn ":memory:" $ \conn -> do
      logInfoN "starting GraphQL server on port 8080"
      liftIO $
        run 8080 $
          graphQLApp
            (prometheus p $ libraryServer conn)
            (Proxy @('Just "Query"))
            (Proxy @('Just "Mutation"))
            (Proxy @('Just "Subscription"))

-- ...more code skipped...

The line p <- initPrometheus "library" is basically all there needs to be to set up tracing with mu-prometheus! We are also able to show logging in our entire app thanks to monad-logger and its runStderrLoggingT function, we start the connection with the database with withSqliteConn, and we start running the server on the port 8080.

-- following the above code...

libraryServer :: SqlBackend -> ServerT ObjectMapping i Library ServerErrorIO _
libraryServer conn =
  resolver
    ( object @"Book"
        ( field @"id" bookId,
          field @"title" bookTitle,
          field @"author" bookAuthor,
          field @"imageUrl" bookImage
        ),
      object @"Author"
        ( field @"id" authorId,
          field @"name" authorName,
          field @"books" authorBooks
        ),
      object @"Query"
        ( method @"authors" allAuthors,
          method @"books" allBooks
        ),
      object @"Mutation"
        ( method @"newAuthor" newAuthor,
          method @"newBook" newBook
        ),
      object @"Subscription"
        (method @"allBooks" allBooksConduit)
    )
  where
    bookId :: Entity Book -> ServerErrorIO Integer
    bookId (Entity (BookKey k) _) = pure $ toInteger k
    bookTitle :: Entity Book -> ServerErrorIO T.Text
    bookTitle = ... -- a lot more resolvers

This is the actual server implementation; each function that we are defining in our where block is a GraphQL resolver, which each extracts a particular piece of data, and then mu-graphql joins everything together, so to speak.

Neat, right? 🤩

Client Side → Elm

Elm is a purely functional language for the frontend, heavily inspired by Haskell, with one of the friendliest compiler error messages in the world 🌎. Seriously! Of course we’d love it at 47 Degrees! 😍

-- ...more code...
view : Model -> Html Msg
view model =
    div []
        [ button [ onClick OpenEditorClicked ] [ text "Add a book" ]
        , button [ onClick SubscribeToBooks, disabled model.isSubscribed ]
            [ text "Try subscriptions" ]
        , input
            [ type_ "text"
            , placeholder "For example, Kant"
            , value model.query
            , onInput QueryChanged
            ]
            []
        , showSearchResponse model.searchResponse
        ]

If you have used Elm before, you already know that the Model is a type that represents the state of all your app, and the type signature for view conveys the idea that it returns some kind of HTML that can produce messages (or dispatch actions, like in Redux) of type Msg.

What about button [ onClick OpenEditorClicked ] [ text "Add a book" ]? Well, if you haven’t written Elm before, maybe the idea of writing DOM nodes using functions (or combinators) is new to you. But it shouldn’t be difficult to grasp; the first parameter it receives is a list of HTML attributes and the second a list of children DOM nodes. Think of this as writing JSX in React, but with extra compiler validation! 💪🏼

Our UI code is going to be quite simple, but we’ll explain where the magic is for the frontend in the next section.

A match made in heaven: mu-graphql + elm-graphql 👼🏼

Let’s look at a more significant piece of code from the frontend:

-- some more imports...
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
import LibraryApi.Object exposing (Author, Book)
import LibraryApi.Object.Author as Author
import LibraryApi.Object.Book as Book
import LibraryApi.Subscription as Subscription

-- ...more irrelevant code omitted...

authorSelection : SelectionSet AuthorData Author
authorSelection =
    SelectionSet.map AuthorData Author.name


bookSelection : SelectionSet BookData Book
bookSelection =
    SelectionSet.map3 BookData
        Book.title
        Book.imageUrl
        (Book.author Author.name)

Did you notice those interesting Book and Author types? Where are they coming from? Well, you’ll be pleasantly surprised to hear that they are also AUTO-GENERATED from our library.graphql schema using Dillon Kearns’s awesome library elm-graphql!!! 🤯🤯🤯

Yes, the whole LibraryApi module is COMPLETELY auto-generated each time from your schema just by running npm run codegen or yarn codegen! 🥰 Of course, you still need to define the queries and the things you want to get from the server. But at least you didn’t have to write the Schema types on your own! 👏🏼

This means that, thanks to the combination of the two libraries (mu-graphql + elm-graphql), our frontend and backend code can’t ever be out of sync! Each side’s compiler will read the GraphQL Schema file and give you compile time errors if something has changed and has not been correctly handled on each side! 🦄🌈

Acknowledgments

We had created this app as an example some time ago, and created Github Issues tagged with the hacktoberfest hashtag to add further improvements whenever we could find some time. That’s why we really have to thank our friend @logachev_dev for his awesome contributions, both to the original Elm code (as well as tons and tons of CSS 💅🏼) and the Haskell code as well! 🙌🏼

Conclusion

We thought this web stack selection was too AWESOME 😎 to not write about it. We hope that you love it too, and you should totally try it out for your future web apps as well!

If you have questions regarding the code or any other doubts, feel free to write to us on Twitter and also by opening Issues in the original repo! 😉

Happy Hacking! 🦄


The active development of Mu-Haskell (and mu-graphql) is proudly sponsored by 47 Degrees, a Functional Programming consultancy with a focus on the Scala, Kotlin, Swift, and Haskell programming languages.

Ensure the success of your project

47 Degrees can work with you to help manage the risks of technology evolution, develop a team of top-tier engaged developers, improve productivity, lower maintenance cost, increase hardware utilization, and improve product quality; all while using the best technologies.