In this tutorial, we will create a tiny imageboard.
The features will be very simple: visitors can see the list of images posted, and some editors will be able to add, modify, or remove images thanks of the GraphQL IDE named GraphiQL. I will explain more about GraphiQL later in this article.
To summarize, an image has a title and a URL to the external website where it's hosted. To keep the application simple, no file is uploaded to the server.
Below you can see how this web application will look.
A live demo version is also available here.
We can summarize the actions possible on this web application with the following use case diagram
Note that all the UML diagrams presented in this article are made with PlantUML.
The image stored in the database can be represented with the following entity-relationship diagram
Now, let's design the architecture!
Postgraphile will generate a GraphQL server from a PostgreSQL database. Then, the editors will be able to create, modify and remove images by communicating directly to GraphQL with GraphiQL. Finally, our Elm web application will allow the visitors to see all the images.
Unlike the other diagrams, the architecture diagram was realized with draw.io.
Create the PostgreSQL database
On PostgreSQL create a database. In this tutorial, we will name it imageboard.
You can use the following command
createdb imageboard
Then, connect to your database to create the table that stores the images.
If you have a local PostgreSQL database instance, you can use psql and execute this command
psql imageboard
Otherwise, you can run the following command
psql postgres://username:password@localhost/imageboard
Replace username and password with the username and the password of your PostgreSQL server. And change localhost by the hostname of your PostgreSQL server.
Once connected, execute this SQL query
CREATE TABLE image (id serial PRIMARY KEY, title VARCHAR(100) NOT NULL, url VARCHAR(2000) NOT NULL);
And insert a test data with this query
INSERT INTO image (title, url) VALUES ('PostgreSQL', 'https://wiki.postgresql.org/images/a/a4/PostgreSQL_logo.3colors.svg');
Create the back office with PostGraphile and GraphQL
It's time to present to you an excellent tool that allows us to easily generate a GraphQL server from a PostgreSQL database. This tool is named Postgraphile.
For those who don't know, GraphQL is an open-source query language and a runtime made to manipulate data through APIs.
GraphQL can be used with HTTP, and a lot of programming language support it on client side or server side (JavaScript, Java, PHP, Swift...). You can see here a detailed list of tools and libraries that allow us to use GraphQL.
The main advantages of GraphQL are:
- A strongly typed language query and a strong type system. This is suitable for reducing the number of errors.
- A mechanism that fetches only what is needed by our application. This is useful when limiting the data sent through the network matters for reducing the latency. For example, on a mobile application that uses limited internet data.
- The possibility to get many resources in a single request.
- Powerful developer tools like GraphiQL.
- The possibility to evolve the API without versions.
This is why compared to the classical REST approach, GraphQL offers a lot of improvements 🚀.
However, implementing an API that uses GraphQL to expose data from a database could take a lot of time. Especially if we want to adjust the database queries and the returned data from the database, regarding the field passed through the GraphQL queries.
Hopefully, by using PostGraphile, this task will be done in a few minutes.
Time to show you how!
First, install PostGraphile. The quick start guide from the official website gives some instructions about that.
We need to install Node.js to have npm.
PostGraphile can be installed globally on your operating system, but in this tutorial, we will install it locally on the project folder.
This approach will allow us to have multiple versions of PostGraphile depending on the project.
Create a directory named imageboard.
Inside this directory, run the following command
npm init -y
npm install postgraphile
After that, execute this command to verify that PostGraphile is correctly installed
npx postgraphile --help
Then, execute PostGraphile
npx postgraphile -c postgres://username:password@localhost/imageboard
Again, note that you must replace username and password with the username and the password of your PostgreSQL server. If needed, change localhost by the hostname of your PostgreSQL server.
By default, the GraphQL server is exposed on the port 5000. You can navigate to the GraphiQL generated by opening the url http://localhost:5000/graphiql on your web browser.
GraphiQL is the IDE of GraphQL and allows us to execute queries and mutations (we will see that later) in a convenient way.
It's also possible to improve the GraphiQL generated by PostGraphile with the option --enhance-graphiql
.
Now, test that you can fetch the inserted image with GraphQL by running this query
{
allImages {
nodes {
title
url
}
}
}
Execute it. This query must return the following
{
"data": {
"allImages": {
"nodes": [
{
"title": "PostgreSQL",
"url": "https://wiki.postgresql.org/images/a/a4/PostgreSQL_logo.3colors.svg"
}
]
}
}
}
You can learn more about GraphQL queries here.
Editors can also use directly GraphiQL to add a new image.
For writing data, GraphQL uses a mechanism named mutation.
In the query editor, write this mutation
mutation AddImage($createImageInput: CreateImageInput!) {
createImage(input: $createImageInput) {
image {
title
url
}
}
}
On the query variables panel, write this JSON text
{
"createImageInput": {
"image": {
"title": "PostGraphile",
"url": "https://www.graphile.org/images/postgraphile.optimized.svg"
}
}
}
Execute the mutation. The following data should be returned
{
"data": {
"createImage": {
"image": {
"title": "PostGraphile",
"url": "https://www.graphile.org/images/postgraphile.optimized.svg"
}
}
}
}
That's all. The editors are now completely autonomous when they manipulate the images.
Providing a back office web application with GraphiQL to some power users, like the editors, is useful. This strategy allows us to ship quickly a proof of concept, or a minimal viable product, to test your business without big developments, and make change faster without making a lot of refactorings. Basically you just have to change the schema of your database and regenerate a GraphQL server with PostGraphile 🪄.
A lot of users don't need a sophisticated UI, and with some minimal documentations and help, they can execute GraphQL queries and mutations.
Furthermore, as a developer, we can define some custom queries and mutations and give them to our power users.
The following sequence diagrams summarize what we do with PostGraphile and GraphQL when the editor create, modify or remove an image:
Note that it's possible to use PostGraphile as a Node.js library. This method is very interesting when we want more control on our GraphQL server. For example, for some security reasons, it can be useful to add authentication by using Node.js.
Develop the front office as a web application with Elm
GraphiQL is enough for the editors to add, modify or remove images. But it's not suitable for the visitors that want to see the list of images.
One of the best ways to respond to this need is to create a custom web application.
To do that, we will use the Elm programming language.
Introduction to ELM
Elm is a functional programming language specially made for the web. That means that an Elm application can be compiled to a JavaScript file and embedded to a HTML one.
Elm provides a wide set of functions for manipulating the DOM, including DOM events, and for making HTTP requests.
The Elm architecture is built with these 3 concepts:
- Model to store the state of the application.
- View to use the state to render the HTML.
- Update to update the state based on messages (usually fired by DOM events).
Some popular JavaScript libraries like Redux has been inspired by this architecture.
Elm can communicate with the outside world with Commands and Subscriptions. It's useful for making HTTP requests, for example.
I said before that Elm is a functional programming language. Precisely, Elm is a statically typed functional programming language that uses only pure functions.
That means that Elm manipulates functions as first-class citizen, when oriented object programming languages, like Java or C++, manipulate mainly classes.
Having pure functions means that each function only uses the values provided by parameters, therefore Elm functions don't use global variables or class attributes (that are like global variables, but that exist only in the scope of the class).
The main benefits of the functional programming are:
- The reduction of the side effects when executing a function: a call to a function with the same parameters produces always the same result.
- The fact that Functions are more suitable for unit testing, because they use only values from its parameters.
- Better maintainability thanks of immutability of values. A variable has a value assigned but this value will never change. This is why it's more a constant rather than a variable.
Elm has also these advantages:
- No runtime exception, thanks of a mechanism that forces the developer to manage the unexpected behaviors of the application, with the concept of error handling.
- Fearless refactoring, with the static typed and pure functional paradigm, and the compiler that produces helpful error messages.
- Great performance, with its implementation of virtual DOM.
- Small assets of the generated JavaScript thanks of the optimizations provided by the compiler.
- JavaScript interoperability with the flags, ports and custom elements.
Manipulate GraphQL with Elm
It's possible to make plain HTTP requests for calling a GraphQL server from an Elm application.
However, a powerful library exists to manipulate GraphQL from Elm. This library is called elm-graphql.
Basically, this library uses our GraphQL server to generate Type-Safe and Composable Elm functions for manipulating GraphQL.
Once you understand how to use the library, it's like magic, and you don't even need to care about raw HTTP requests and GraphQL details 🧙​.
Setup the application
Now it's time to start to create the web application.
To begin, install Elm using the official guide instructions.
Inside the imageboard directory, run the following command
elm init
That will initialize our Elm application.
Say yes when you are asked to create the elm.json file. This file lists the Elm dependencies with the version of each package used by our application.
Then, run this command to install elm-graphql and confirm the modification of elm.json
elm install dillonkearns/elm-graphql
Run the following command to generate the functions and types that manipule GraphQL (be sure that PostGraphile is still running)
npx @dillonkearns/elm-graphql --base Imageboard http://localhost:5000/graphql
You can change the hostname and the port according to how you run PostGraphile.
A folder named Imageboard should have been generated, inside the src folder, and contains all our Elm source codes.
Now, create a file named Main.elm inside the src folder.
Let's start creating the scaffolding of our Elm application.
First we create the main function that calls the Browser.element function.
module Main exposing (main)
import Browser
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
Browser.element is used instead of Browser.sandbox because the application need to communicate with the outside world. In particular when making HTTP requests to the GraphQL server.
Then we create the init function that will be executed when the web application starts.
init : () -> ( Model, Cmd Msg )
init _ =
( NotAsked, Cmd.none )
The first parameter of the init function represents the flags passed from outside the Elm application (for example from the HTML file that embed this application). It's useful in certain circumstances, but in our case, we can use the unit type with the value (). This type is the equivalent of the type void in some other programming languages.
The init function returns a value of type Model that represents the state of our application and returns a Command that should be executed.
For the moment, we put Cmd.none, but we will replace it by the function that makes a request to our GraphQL server.
Note that NotAsked is one of the possible variant of our Model that is defined bellow
-- others imports are omitted
import RemoteData exposing (RemoteData(..))
-- others functions are omitted
type alias Model =
RemoteData (Error Response) Response
type alias Error parsedData = parsedData
type alias Response =
Maybe (List (Maybe Image))
type alias Image =
{ title : String
, url : String
, id : Int
}
The Model is an alias (using type alias) for the type RemoteData that represents a result of a request made to a server.
krisajenkins/remotedata is an external library that works well with dillonkearns/elm-graphql and simplify the management of HTTP requests.
Because that RemoteData, from the library krisajenkins/remotedata, is a custom type, it has some possible variants like NotAsked, Loading, Failure or Success.
To use this library, run the following command in the root application folder
elm install krisajenkins/remotedata
The type RemoteData has 2 type variables named e and a. These type variables respectively represent the type of data raised when the request fails (the variant Failure), and the type of data raised when the request succeeds (the variant Success).
We create the type alias Response for replacing the type variable a, and the type alias Error to replace the type variable e.
Note that a type alias Error is just a mock type, and take a type variable named parsedData and return it. Once we use elm-graphql, the type Error should be replaced by the one provided by the library and that has the same type description.
As you can see, when the value of the custom type RemoteData represents success, this success is linked to a type Response that is an optional list of optional Image. Optional types are managed with the type Maybe.
Note that we can use directly the type RemoteData (Error (Maybe (List (Maybe Image)))) (Maybe (List (Maybe Image))) instead of creating an intermediate type Response. However, creating a type alias Response avoids the duplication of the type Maybe (List (Maybe Image)).
The type alias Image is related to the table image in our PostgreSQL database.
Add these instructions that manages the subscriptions
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
Since we don't need to subscribe to some events, this function returns the value Sub.none.
Create a simple function update with the following content
update : Msg -> Model -> ( Model, Cmd Msg )
update _ model =
( model, Cmd.none )
and a simple type Msg
type alias Msg =
()
For the view, add the instructions bellow
view : Model -> Html Msg
view model =
case model of
NotAsked ->
div [] [ text "Images are not yet asked." ]
Loading ->
div [] [ text "Images are loading..." ]
Failure _ ->
div [] [ text "There was a failure when we try to load the image." ]
Success response ->
case response of
Nothing ->
div [] [ text "No response could be retrieve from the server." ]
Just images ->
div
[]
[ h1 [] [ text "Imageboard" ]
, div [ class "gallery" ] (List.map viewImage images)
]
viewImage : Maybe Image -> Html Msg
viewImage maybeImage =
case maybeImage of
Nothing ->
div [] [ text "No data could be retrieved from the server." ]
Just image ->
img
[ src image.url, Html.Attributes.id (fromInt image.id), alt image.title ]
[]
In the top of the file, when we import modules, add these new imports to have the functions used by the view
import Html exposing (Html, div, h1, img, text)
import Html.Attributes exposing (alt, class, src)
import String exposing (fromInt)
The view is relatively simple.
The value of the model, that is an equivalent of RemoteData (Error (Maybe (List (Maybe Image)))) (Maybe (List (Maybe Image))), is tested using pattern matching and a different HTML is render regarding the value of the variant that represents the state of the request made.
Elm uses the package elm/html, available in the standard library, for generating HTML tags with the good values for the HTML attributes.
This package provides functions that generate almost every HTML tags and return a value of type Html, that is the type needed for the root view function.
For rendering all the image, the List.map function is used. In functional programming, instead of using loop with for or while keywords, we generally use map, fold or filter functions (or the equivalents).
The map function takes for the first parameter a function to be executed on each element of the list, and for the second parameter the list himself. Here, the function viewImage is executed and renders the HTML tag img.
The file has now this content
module Main exposing (main)
import Browser
import Html exposing (Html, div, h1, img, text)
import Html.Attributes exposing (alt, class, src)
import RemoteData exposing (RemoteData(..))
import String exposing (fromInt)
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
init : () -> ( Model, Cmd Msg )
init _ =
( NotAsked, Cmd.none )
update : Msg -> Model -> ( Model, Cmd Msg )
update _ model =
( model, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
view : Model -> Html Msg
view model =
case model of
NotAsked ->
div [] [ text "Images are not yet asked." ]
Loading ->
div [] [ text "Images are loading..." ]
Failure _ ->
div [] [ text "There was a failure when we try to load the image." ]
Success response ->
case response of
Nothing ->
div [] [ text "No response could be retrieve from the server." ]
Just images ->
div
[]
[ h1 [] [ text "Imageboard" ]
, div [ class "gallery" ] (List.map viewImage images)
]
viewImage : Maybe Image -> Html Msg
viewImage maybeImage =
case maybeImage of
Nothing ->
div [] [ text "No data could be retrieved from the server." ]
Just image ->
img
[ src image.url, Html.Attributes.id (fromInt image.id), alt image.title ]
[]
type alias Model =
RemoteData (Error Response) Response
type alias Error parsedData = parsedData
type alias Response =
Maybe (List (Maybe Image))
type alias Image =
{ title : String
, url : String
, id : Int
}
type alias Msg =
()
In the root folder, create a file named index.html with the following content
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Imageboard</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="elm-node"></div>
<script type="text/javascript" src="main.js"></script>
<script>
Elm.Main.init({
node: document.getElementById("elm-node")
});
</script>
</body>
</html>
and a file named styles.css with this content
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f5f5f5;
margin: 0;
}
h1 {
font-size: 32px;
font-weight: bold;
color: #0077cc;
text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
margin-bottom: 20px;
text-align: center;
}
.gallery {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
max-width: 400px;
padding-bottom: 20px;
}
.gallery img {
width: 100%;
max-width: 350px;
height: auto;
border: 5px solid #0077cc;
border-radius: 15px;
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.2);
padding: 10px;
background-color: #ffffff;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.gallery img:hover {
transform: scale(1.05);
box-shadow: 8px 8px 15px rgba(0, 0, 0, 0.3);
}
The HTML content references the CSS file and the JavaScript file that will be generated from the Elm compiler.
The JavaScript instructions bellow are used to initialize the application generated from our Elm source code and embed it inside the tag div with the id elm-node.
Elm.Main.init({
node: document.getElementById("elm-node")
});
These CSS instructions enhance the design of the web application and provide some cool effects when the visitor moves his cursor over an image.
Run the command elm make src/Main.elm --output=main.js
to generate the JavaScript file.
Open with your web browser the file index.html and observe the result.
An empty page with only the text Images are not yet asked. should be displayed.
Now we will modify the source code to make the request to our GraphQL server.
Change the init function with the following code
init : () -> ( Model, Cmd Msg )
init _ =
( Loading, makeRequest "http://localhost:5000/graphql" )
We will define the makeRequest function later. Of course, change the URL to the GraphQL server if needed.
Change also the update function with this code
update : Msg -> Model -> ( Model, Cmd Msg )
update msg _ =
case msg of
GotResponse response ->
( response, Cmd.none )
Replace the type Msg by the following
type Msg
= GotResponse Model
Now we handle a message of the custom type Msg with one variant: GotResponse.
The next functions: makeRequest and query are the most difficult to understand.
The goal of the function makeRequest is to send the HTTP request, returned by the function query, to our GraphQL server.
The function query has the goal to generate the GraphQL query seen before
{
allImages {
nodes {
title
url
id
}
}
}
A function named allImages has been generated with elm-graphql and is available in the module Imageboard.Query. This module regroups all root level GraphQL queries.
This function takes for the first parameter a function that has the following signature AllImagesOptionalArguments -> AllImagesOptionalArguments. This is because the allImage query generated by PostGraphile takes some parameters that represent the WHERE clause, the ORDER BY clause and the LIMIT clause of the SQL query performed. That's useful for paginating and filtering purposes.
The second parameter of this function, and maybe the most difficult to understand, is a SelectionSet decodesTo Imageboard.Object.ImagesConnection. The SelectionSet is a type built by elm-graphql to select the fields that should be put in our query.
With PostGraphile, it represents the SELECT query.
Because query in GraphQL can have sub queries as fields, each query function returns a SelectionSet with a given scope and takes a SelectionSet with another scope.
A SelectionSet takes the scope as the second parameter, and as a first parameter, it takes a function that will be used to transform the result of the GraphQL query, depending on the fields passed, to an Elm value or record.
Before doing the allImages query, start by implementing the nodes query.
The associated function of the nodes query is the nodes function available in the module named Imageboard.Object.ImagesConnection. The signature of this function is SelectionSet decodesTo Imageboard.Object.Image -> SelectionSet (List (Maybe decodesTo)) Imageboard.Object.ImagesConnection.
Once the nodes function called with one value for the first parameter named object____, it returns a value of type SelectionSet (List (Maybe decodesTo)) Imageboard.Object. That's exactly what is needed for the function allImages. The second parameter of this function, also named object____, is a value of type SelectionSet decodesTo Imageboard.Object.ImagesConnection. This is why we will call the function nodes and pass the result of the function allImages as its second parameter.
But what should we pass to the first parameter of the function allImages? Remember, we must pass a value of type SelectionSet decodesTo Imageboard.Object.Image that represents the fields asked to GraphQL.
In this query, we want the field title, url and id.
Hopefully elm-graphql has generated the functions title, url and id inside the module Imageboard.Object.Image.
The title and url functions return a value of type SelectionSet String Imageboard.Object.Image whereas the function id returns a value of type SelectionSet Int Imageboard.Object.Image.
In the function nodes we should define a concrete type for the type variable decodesTo of the first parameter SelectionSet decodesTo Imageboard.Object.Image. This is here that we use the type Image defined for the Model.
To bind the generated functions title, url and id to the type Image, elm-graphql provides a function named map3 that takes a function that creates a value of a given type, with this signature (decodesTo1 -> decodesTo2 -> decodesTo3 -> decodesToCombined) -> SelectionSet decodesTo1 scope -> SelectionSet decodesTo2 scope -> SelectionSet decodesTo3 scope -> SelectionSet decodesToCombined scope, and 3 other parameters of respectively types SelectionSet decodesTo1 scope, SelectionSet decodesTo2 scope and SelectionSet decodesTo3 scope.
Note that the library elm-graphql provides several mapN functions depending of the number of fields we want to map (map, map2, ..., map8).
In our case, we want that the decodesTo1 becomes the type String, decodesTo2 becomes String, decodesTo3 becomes Int, decodesToCombined becomes Image and scope becomes Imageboard.Object.Image to have this signature for the function map3 : (String -> String -> Int -> Image) -> SelectionSet String Imageboard.Object.Image -> SelectionSet String Imageboard.Object.Image -> SelectionSet Int Imageboard.Object.Image -> SelectionSet Image Imageboard.Object.Image.
It's important to notice that in Elm each type alias is a function. This concept is called Record Constructors. Due to that, the type alias Image is also a function with this signature Image : String -> String -> Int -> Image. Also, remember that in Elm if we call a function without providing all the parameters, then the returned value is a function with the remaining parameters. For example, if we use the type Image as a function and call it in this way Image "PostGraphile"
, the returned value is String -> Int -> Image. If we call Image like that Image "PostgreSQL" "https://wiki.postgresql.org/images/a/a4/PostgreSQL_logo.3colors.svg"
, the returned value is Int -> Image. Finally, if we do Image "PostgreSQL" "https://wiki.postgresql.org/images/a/a4/PostgreSQL_logo.3colors.svg" 1
, the returned value is Image. The last call of the type alias Image is equivalent to this record { title = "PostgreSQL", url = "https://wiki.postgresql.org/images/a/a4/PostgreSQL_logo.3colors.svg", id = 1 }
.
This is why we can call the function map3 like that: map3 Image title url id
.
Now we go back to the function allImages. For the first parameter of type (AllImagesOptionalArguments -> AllImagesOptionalArguments), we can use the identity function, that just returns the argument passed as parameter. For the second parameter of type SelectionSet decodesTo Imageboard.Object.ImagesConnection, the decodesTo parameter value should be replaced by the type Image to match the value returned by the expression map3 Image title url id.
Note that the queries generated by PostGraphile often return an optional value. For this reason, the functions nodes and allImages return a Maybe type. Due to that the returned type of the function allImages is SelectionSet (Maybe (List (Maybe Image))) RootQuery.
Now the query function is like that
query : SelectionSet (Maybe (List (Maybe Image))) RootQuery
query =
allImages identity (nodes (map3 Image title url id))
Don't forget to add these import statements
import Graphql.Operation exposing (RootQuery)
import Graphql.SelectionSet exposing (SelectionSet, map3)
import Imageboard.Object.Image exposing (id, title, url)
import Imageboard.Object.ImagesConnection exposing (nodes)
import Imageboard.Query exposing (allImages)
For making HTTP request to the GraphQL server, the function send of the module Graphql.Http can be used. The signature of this function is (Result (Error decodesTo) decodesTo -> msg) -> Request decodesTo -> Cmd msg. The type variable msg will be replaced by the custom type Msg that we defined before. The type variable decodesTo will be replaced by the type Maybe (List (Maybe Image)). The same type that is returned by our function query. Note that the type Request is defined in the module Graphql.Http.
The function queryRequest of the module Graphql.Http creates a value of type Request. Its signature is String -> SelectionSet decodesTo RootQuery -> Request decodesTo. The first parameter is the url of the GraphQL server and the second is a SelectionSet. In our case, the second parameter will be the returned value of the call of our function query, to have a value of type SelectionSet (Maybe (List (Maybe Image))) RootQuery for the second parameter, and a returned value of type Request (Maybe (List (Maybe Image))) for the function queryRequest. A possible call of this function could be queryRequest graphQLUrl query
with graphQLUrl a constant that contains a value of type String that represents the url of our GraphQL server (in our case it could be "http://localhost:5000/graphql").
So, the result of the function queryRequest could be passed to the second parameter of the function send. For the first parameter, we can use the combination of the function fromResult defined in the module RemoteData (provided by the library krisajenkins/remotedata) and the variant GotResponse of the custom type Msg. The function send need for the first parameter a value of type (Result (Error decodesTo) decodesTo -> msg), with decodesTo mapped to the type Maybe (List (Maybe Image)), and msg mapped to the type Msg. Due to that, it need the type (Result (Error (Maybe (List (Maybe Image)))) decodesTo -> Msg).
One important thing to understand is that the variant GotResponse of the custom type Msg is a function with this signature: Model -> Msg. That's because each variant of a custom type is a function that returns the custom type himself. If this variant has some associated types, then the related function takes parameters of these types before returning the custom type.
This is why GotResponse is a function with the signature Model -> Msg, but Model is a type alias for RemoteData (Error Response) Response. Due to the fact that Response is a type alias for Maybe (List (Maybe Image)), then GotResponse has this signature: RemoteData (Error (Maybe (List (Maybe Image)))) (Maybe (List (Maybe Image))) -> Msg.
fromResult has this signature: Result e a -> RemoteData e a, so in our case the signature becomes Result (Error (Maybe (List (Maybe Image)))) (Maybe (List (Maybe Image))) -> RemoteData (Error (Maybe (List (Maybe Image)))) (Maybe (List (Maybe Image))).
In Elm, like in other functional programming language, we can use the function composition to create a new function from the two that we combined. The new function will take the parameters of the first function used and will return the same type of the second function used. In Elm, the operator >> is used to perform a function composition. Therefore, the result of the instruction fromResult >> GotResponse
is a function with this signature Result (Error (Maybe (List (Maybe Image)))) (Maybe (List (Maybe Image))) -> Msg. And the following instruction send (fromResult >> GotResponse) (queryRequest graphQLUrl query)
returns a value of type Cmd Msg.
We now have our final function makeRequest with the following code
makeRequest : String -> Cmd Msg
makeRequest graphQLUrl =
send (fromResult >> GotResponse) (queryRequest graphQLUrl query)
And this module with the import statement
import Graphql.Http exposing (Error, queryRequest, send)
Modify the import of the module RemoteData with the following code
import RemoteData exposing (RemoteData(..), fromResult)
You can remove the Error type defined when we created the Model.
-- Remove this code because now we use the Error type defined in the module Graphql.Http
type alias Error parsedData = parsedData
Install the module elm/json, that is required by the code generated by elm-graphql, and confirm the move to the direct dependencies
elm install elm/json
The final source code of our Elm application is like that
module Main exposing (main)
import Browser
import Graphql.Http exposing (Error, queryRequest, send)
import Graphql.Operation exposing (RootQuery)
import Graphql.SelectionSet exposing (SelectionSet, map3)
import Html exposing (Html, div, h1, img, text)
import Html.Attributes exposing (alt, class, src)
import Imageboard.Object.Image exposing (id, title, url)
import Imageboard.Object.ImagesConnection exposing (nodes)
import Imageboard.Query exposing (allImages)
import RemoteData exposing (RemoteData(..), fromResult)
import String exposing (fromInt)
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
init : () -> ( Model, Cmd Msg )
init _ =
( Loading, makeRequest "http://localhost:5000/graphql" )
makeRequest : String -> Cmd Msg
makeRequest graphQLUrl =
send (fromResult >> GotResponse) (queryRequest graphQLUrl query)
query : SelectionSet (Maybe (List (Maybe Image))) RootQuery
query =
allImages identity (nodes (map3 Image title url id))
update : Msg -> Model -> ( Model, Cmd Msg )
update msg _ =
case msg of
GotResponse response ->
( response, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
view : Model -> Html Msg
view model =
case model of
NotAsked ->
div [] [ text "Images are not yet asked." ]
Loading ->
div [] [ text "Images are loading..." ]
Failure _ ->
div [] [ text "There was a failure when we try to load the image." ]
Success response ->
case response of
Nothing ->
div [] [ text "No response could be retrieve from the server." ]
Just images ->
div
[]
[ h1 [] [ text "Imageboard" ]
, div [ class "gallery" ] (List.map viewImage images)
]
viewImage : Maybe Image -> Html Msg
viewImage maybeImage =
case maybeImage of
Nothing ->
div [] [ text "No data could be retrieved from the server." ]
Just image ->
img
[ src image.url, Html.Attributes.id (fromInt image.id), alt image.title ]
[]
type alias Model =
RemoteData (Error Response) Response
type alias Response =
Maybe (List (Maybe Image))
type alias Image =
{ title : String
, url : String
, id : Int
}
type Msg
= GotResponse Model
As we did before, run again this command elm make src/Main.elm --output=main.js
to regenerate the JavaScript file.
Open the file index.html
with your web browser and check that all the images inserted in the PostgreSQL database are well displayed ✨.
If you have some troubles, verify that the GraphQL server generated by PostGraphile is still running. If needed, execute again the command postgraphile
.
Note that to avoid cors issue, you must add the --cors
option when running the command postgraphile
.
The reason is explained in the official documentation. It's possible to avoid using unrestricted CORS settings by using PostGraphile as a library, inside a Node.js program that will serve our static files.
Bonus: some syntactic sugars
If you look on some examples of the elm-graphql documentation, you can notice that the operator |>
is used when making the GraphQL query.
You can find more information about this operator in the Elm documentation.
Basically, instead of doing these instructions
makeRequest : String -> Cmd Msg
makeRequest graphQLUrl =
send (fromResult >> GotResponse) (queryRequest graphQLUrl query)
we can do that
makeRequest : String -> Cmd Msg
makeRequest graphQLUrl =
query
|> queryRequest graphQLUrl
|> send (fromResult >> GotResponse)
This way to write the code is named the pipeline syntax.
Also, it's possible to use the keyword as when importing a module to have an alias name for this module. This is useful to quickly see that a given function comes from the library elm-graphql. Also, we don't need to import the function that we use during the import section.
Basically, the new source code of the functions makeRequest and query are
makeRequest : String -> Cmd Msg
makeRequest graphQLUrl =
query
|> Graphql.Http.queryRequest graphQLUrl
|> Graphql.Http.send (fromResult >> GotResponse)
query : SelectionSet (Maybe (List (Maybe Image))) RootQuery
query =
Query.allImages identity
(ImagesConnection.nodes
(SelectionSet.map3 Image Image.title Image.url Image.id)
)
and the import statements become
-- Only the import statements modified are shown
import Graphql.Http exposing (Error)
import Graphql.SelectionSet as SelectionSet exposing (SelectionSet)
import Imageboard.Object.Image as Image
import Imageboard.Object.ImagesConnection as ImagesConnection
import Imageboard.Query as Query
Conclusion
You learn how to use PostgreSQL, GraphQL, PostGraphile, Elm and elm-graphql for creating a simple imageboard.
The source code is available in this git repository with some enhancements.
You can make some improvements in your side, if you want. For example by using GraphQL mutation inside our Elm application for adding an image, or by combining elm-graphql, PostGraphile and elm-paginate to manage the pagination.
If you want to go deeper, you can check the following links:
Top comments (0)