Building a GORM/GraphQL App with React and Apollo
Build a Grails app and use GORM's GraphQL support to serve React app using Apollo
Authors: Zachary Klein
Grails Version: 4.0.0
1 Training
Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.
2 Getting Started
In this guide you will learn how to leverage GORM’s support for GraphQL. You will configure Grails to act as a GraphQL server,
The initial
sample project already has a simple conference-related REST API (consisting of two domain classes, Speaker
and Talk
) configured using the @Resource annotation. The project is using the react profile, and a simple React app has been provided which will list out the speakers, as well as the talks belonging to each speaker.
The React app is quite trivial and naively uses the default REST API provided by Grails - e.g, the app makes one REST call to list the speakers, and then iterates over the talk ids for each speaker, making a separate REST call to get the details for each talk. This means that a single load of the app can require many REST calls simply to acquire the data needed to render the app.
http://localhost:8080/speaker 200 fetch 1.6 KB 851 ms
http://localhost:8080/talk/1 200 fetch 260 B 262 ms
http://localhost:8080/talk/2 200 fetch 273 B 310 ms
http://localhost:8080/talk/3 200 fetch 254 B 299 ms
http://localhost:8080/talk/4 200 fetch 261 B 297 ms
http://localhost:8080/talk/5 200 fetch 293 B 284 ms
Of course there are a few ways we could resolve this problem using standard REST techniques, including hypermedia, or customized endpoints (using JSON Views, for example) - however in more complex apps it is possible that this could lead to a proliferation of custom endpoints providing different combinations of data. In addition, some endpoints might provide more data than the app actually requires - there is no way to express the desired "piece" of data in a typical REST API.
This is where GraphQL shines - it allows consumers of our API, whether private clients (like our own Single-Page Applications) or public users, to specify the exact pieces of data that they need, in a declarative way. GraphQL queries can even be codifed as Flow types, making the data requirements of your components explicit and testable.
Please refer to the excellent documentation and other resources at the official GraphQL project website for more information.
2.1 What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE
-
JDK 8 or greater installed with
JAVA_HOME
configured appropriately
2.2 How to complete the guide
To get started do the following:
-
Download and unzip the source
or
-
Clone the Git repository:
git clone https://github.com/grails-guides/gorm-graphql-with-react-and-apollo.git
The Grails guides repositories contain two folders:
-
initial
Initial project. Often a simple Grails app with some additional code to give you a head-start. -
complete
A completed example. It is the result of working through the steps presented by the guide and applying those changes to theinitial
folder.
To complete the guide, go to the initial
folder
-
cd
intograils-guides/gorm-graphql-with-react-and-apollo/initial
and follow the instructions in the next sections.
You can go right to the completed example if you cd into grails-guides/gorm-graphql-with-react-and-apollo/complete
|
3 Configuring GORM for GraphQL
Install the GORM GraphQL plugin by adding this line to the dependencies
section in build.gradle
:
compile "org.grails:gorm-graphql:1.0.2"
compile "org.grails.plugins:gorm-graphql:1.0.2"
Now, to generate a GraphQL schema for your domain classes, all that is needed is to add a static graphql
property to the domain class in question.
package com.objectcomputing
import grails.rest.Resource
@Resource(uri='/talk')
class Talk {
String title
int duration
static graphql = true (1)
static belongsTo = [speaker: Speaker]
}
1 | This will cause a GraphQL schema to be generated based on the properties in the domain class, following the constraints specified in the constraints block. |
If you need to customize the behavior of the GraphQL schema, you can assign the graphql
property to a custom GraphQLMapping
, using the DSL provided by the plugin. This is not a requirement for this guide, however a few of the supported options are shown in the Speaker
domain class.
package com.objectcomputing
import grails.rest.Resource
import org.grails.gorm.graphql.entity.dsl.GraphQLMapping
import java.time.LocalDate
import java.time.Period
@Resource(uri='/speaker')
class Speaker {
String firstName
String lastName
String name
String email
String bio
LocalDate birthday
static hasMany = [talks: Talk]
static graphql = GraphQLMapping.build {
property 'lastName', order: 1 (1)
property 'firstName', order: 2
property 'email', order: 3
exclude 'birthday' (2)
property 'name', deprecationReason: 'To be removed August 1st, 2020' (3)
property('bio') { (4)
order 4
dataFetcher { Speaker speaker ->
speaker.bio ?: "No biography provided"
}
}
add('age', Integer) { (5)
dataFetcher { Speaker speaker ->
Period.between(speaker.birthday, LocalDate.now()).years
}
input false
}
}
static constraints = {
email nullable: true, email: true
birthday nullable: true
bio nullable: true
}
static mapping = {
bio type: 'text'
name formula: 'concat(FIRST_NAME,\' \',LAST_NAME)'
talks sort: 'id'
}
}
1 | Set property ordering |
2 | Exclude property from the schema |
3 | Deprecate a property (can also be set to deprecated: true ) |
4 | Custom data fetcher, for providing custom logic when retrieving the property |
5 | Adding a (non persistent) property to the GraphQL schema |
You can learn all the available features of the GORM GraphQL plugin from the plugin documentation. |
4 Playing with the GraphQL browser
The GORM GraphQL plugin provides an interactive GraphQL browser (GraphiQL) which makes it easy to explore the GraphQL schema. The browser is available when the Grails app is running, at the URL /graphql/browser
. The browser features include autocompletion based on your schema, making it easy to explore the available types of queries and mutations (GraphQL terminology for requests that create/update/delete data from the schema).
Start up the app with either grails run-app
or ./gradlew bootRun
, and browse to http://localhost:8080/graphql/browser
.
You can now run queries against your new GraphQL schema, as well as mutations. Here’s a few queries and mutations to get your started - refer to the GORM GraphQL plugin documentation and the GraphQL project website to learn more about the syntax and features of GraphQL.
Queries
//List speaker ids
query {
speakerList(max: 10) {
id
}
}
//Return total speakers
query {
speakerCount
}
//Retrieve speaker details by id
query {
speaker(id: 1) {
firstName
lastName
bio
}
}
//Return list of talks with speaker details embedded
query {
talkList(max:10) {
title
speaker {
firstName
lastName
}
}
}
Mutations
//Update speaker by id, return any error messages (if any)
mutation {
speakerUpdate(id: 1, speaker: {
bio: "Updated bio!"
}) {
id
bio
errors {
field
message
}
}
}'
//Create speaker - this mutation will return an error due to missing property
mutation {
speakerCreate(speaker: {
firstName: "James"
lastName: "Kleeh"
}) {
id
firstName
lastName
errors {
field
message
}
}
}
//Delete speaker by id
mutation {
speakerDelete(id: 2) {
error
}
}
5 Install and Configure Apollo
Now that you have a GraphQL schema, it is time to replace the existing RESTful integration in the React app using GraphQL. Of course we could compose GraphQL queries manually, however in most cases you will be better served by using a library to abstract away the details of making these requests. One option is the Relay framework, which is developed by Facebook (the creators of React). Another popular library is Apollo, which offers bindings for numerous JavaScript frameworks, including Angular and React. This guide will be using Apollo in the following steps.
Install the necessary packages using either npm
or yarn
:
~ npm install apollo-client-preset react-apollo graphql-tag graphql --save
The first requirement to use Apollo is to configure a provider, which will encapsulate the details needed to actual make requests to the GraphQL schema. Edit the file src/main/webapp/index.js
as shown below:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './css/bootstrap.css';
import './css/App.css';
import './css/grails.css';
import './css/main.css';
import {ApolloProvider} from 'react-apollo';
import {ApolloClient} from 'apollo-client';
import {createHttpLink} from 'apollo-link-http';
import {InMemoryCache} from 'apollo-cache-inmemory';
import { SERVER_URL} from "./config";
(1)
const client = new ApolloClient({
link: createHttpLink({ uri: `${SERVER_URL}/graphql` }), (2)
cache: new InMemoryCache()
});
(3)
ReactDOM.render(< ApolloProvider client={client}>
<App />
</ApolloProvider>, document.getElementById('root'));
1 | We create an instance of ApolloClient , which requires two objects - an HttpLink (which represents the URL of the GraphQL schema) and a cache store. |
2 | This client will send queries to the ${SERVER_URL}/graphql endpoint - which conveniently happens to be where the GORM GraphQL plugin exposes the GraphQL schema |
3 | Finally, we wrap our App component with an ApolloProvider component, passing our ApolloClient instance as a prop to the latter. Now all child components will be able to query/mutate the GraphQL schema pointed to by the client . |
6 Querying GraphQL
Our React app is fairly simple, consisting of 3 main components, SpeakerList
, Speaker
, and Talk
. The first two of these components include REST calls to load their data. Let’s replace these calls with a GraphQL query.
Edit src/main/webapp/SpeakerList.js
import React, {Component} from 'react'
import Speaker from "./Speaker";
import {graphql} from 'react-apollo'; (1)
import gql from 'graphql-tag'; (2)
class SpeakerList extends Component {
(3)
render() {
const speakers = this.props.data.speakerList; (4)
return speakers ? [speakers.map(s => <Speaker key={s.id} speaker={s}/>)] : <p>Loading...</p>
}
}
const SPEAKER_QUERY = gql`query { speakerList(max: 10) { (5)
id, firstName, lastName,
talks { id }
}
}`;
(6)
const SpeakerListWithData = graphql(SPEAKER_QUERY,
{options: {pollInterval: 5000}} (7)
)(SpeakerList);
export default SpeakerListWithData; (8)
1 | The graphql is a higher-order component, which means that it wraps an existing component and returns a new component with additional behavior. |
2 | gql is JavaScript template literal tag that will parse our GraphQL queries |
3 | Note that we were able to completely remove our componentDidMount method (where the REST call had been made) and our constructor, as we no longer need to use this.state . |
4 | The data returned by our GraphQL query will be available via this.props.data.[name of query] - in this case, speakerList . |
5 | This constant represents our query - note that the syntax is the same as what you might have used in the GraphiQL browser. |
6 | Rather than exporting SpeakerList directly, we first "wrap" it using the graphql higher-order component, which will "connect" our SpeakerList component with our GraphQL schema, pass in our data requirements, and provide the data via the data prop within SpeakerList . |
7 | We’ve specified a pollInterval , which is one of several options available from graphql . This option will cause our GraphQL query to be run once every 10 seconds, updating our component with any new data automatically. |
8 | Finally, we export the "connected" version of SpeakerList as the default module. |
At this point, if you restart the application (or run ./gradlew webpack
in another terminal, to reload the JavaScript), you will see the React app is now retrieving the speakers via the GraphQL schema.
Moving on the the Speaker
component, we can again remove the componentDidMount
method, and we can remove the this.state.talks
property. Instead, in the render
method, we will access each speakers' talks directly from the speaker
prop.
import React, {Component} from 'react'
import {Well} from 'react-bootstrap'
import Talk from "./Talks";
class Speaker extends Component {
constructor() {
super();
this.state = {
title: '',
duration: ''
}
}
//... other methods ommitted for space
render() {
const {speaker} = this.props;
const {title, duration} = this.state;
return <Well>
<h3>{speaker.firstName} {speaker.lastName}</h3>
<hr/>
<ul>
{speaker.talks.map(t => <Talk key={t.id} talk={t} delete={() => this.deleteTalk(t.id)}/>)}
<li>
<label>Title</label>
<input type='text' name='title' value={title}
onChange={this.handleNewTalkChange}/>
<label>Duration</label>
<input type="number" name="duration" value={duration}
onChange={this.handleNewTalkChange}/>
<button onClick={this.addNewTalk}>Add Talk</button>
</li>
</ul>
</Well>
}
}
export default Speaker;
<1> <2>
However, if we try to run the app now we will find that we only have access to the ids of each talk - we are missing title
and duration
. Thanks to GraphQL, we can easily add these properties to our retrieved data by simply adding them to the query.
Edit src/main/webapp/SpeakerList.js
and add title
and duration
to the SPEAKER_QUERY
.
const SPEAKER_QUERY = gql`query {
speakerList(max: 10) {
id, firstName, lastName,
talks { id, title, duration } (1)
}
}`;
With that, our multiple REST calls have been replaced with a single GraphQL query. You can inspect this query, and the response, using your Apollo’s developer tools browser extension.
7 Mutations
As mentioned earlier, mutations are GraphQL’s mechanism for changing the data via the GraphQL schema. The available mutations are defined in the schema. The GORM GraphQL plugin generates several basic mutations, described in the documentation. For example, for our Speaker
domain class, the plugin provides the following mutations:
-
speakerCreate(book: {})
-
speaker(id: .., book: {})
-
speaker(id: ..)
Of course, custom mutations can be defined as well - the plugin documentation describes how.
Our React app already uses REST calls to create and delete talks for a given speaker. Let’s replace these REST calls with GraphQL mutations.
Edit /client/src/Speaker.js
:
const talkCreate = gql`
mutation talkCreate($talk: TalkCreate!) {
talkCreate(talk: $talk) {
id
title
duration
}
}
`;
const talkDelete = gql`
mutation talkDelete($id: Long!) {
talkDelete(id: $id) {
error
}
}
`;
We’ve defined GraphQL fragments for two mutations, talkCreate
and talkDelete
. These fragments specify the inputs required by each mutation, and their types. You can type these mutations into the GraphiQL browser and see their required inputs.
Now we need to bind these mutations to our component props, so we can call them from our component. Again, we use the higher-order graphql
component for this task. Because we have multiple mutations, we will import Apollo’s compose
function to streamline the "chaining" of these mutations.
First, import the compose
function at the top of Speaker.js
:
import {graphql, compose} from 'react-apollo'
import gql from 'graphql-tag'
Now we can use compose
to bind these mutations to our Speaker
component.
const SpeakerWithMutations = compose(
graphql(talkCreate, {
props: ({mutate}) => ({
talkCreate: ({talk}) =>
mutate({
variables: {talk},
})
})
}),
graphql(talkDelete, {
props: ({mutate}) => ({
talkDelete: ({id}) =>
mutate({
variables: {id},
})
})
})
)(Speaker)
export default SpeakerWithMutations;
For more information regarding the use of mutations and compose , please refer to the Apollo documentation.
|
The last step is to call our mutations in place of REST calls. Find the addNewTalk
and deleteTalk
functions in Speaker.js
, and edit them as shown below:
addNewTalk = () => {
const {title, duration} = this.state;
const {speaker} = this.props;
this.setState({title: '', duration: ''});
this.props.talkCreate({talk: {title, duration, speaker: {id: speaker.id}}})
.then(({data}) => console.log('create response:', data))
.catch((error) => console.log('there was an error sending the query', error));
};
deleteTalk = (id) => {
this.props.talkDelete({id: id})
.then(({data}) => console.log('delete response: ', data))
.catch((error) => console.log('there was an error sending the query', error));
};
Each of the mutations we’ve bound to our component are available as functions off of this.props
: talkCreate()
and talkDelete()
. Each function takes a single argument, which is a JavaScript object containing all the parameters required by our mutation. When the mutation resolves, the then()
function will be called with the data that is returned by the mutation. For this sample, we are simply logging out the data, but you could also use this to update the Apollo cache as described in the documentation.
For simplicity, this guide is relying on the pollingInterval we set up earlier to refresh the data and remove/add talks from the cache. In a real-world application, you will want to update the data in memory using the result of the mutation.
|
8 Conclusion
In this guide we’ve seen how easy is to create a full-featured GraphQL schema with Grails, and to interact with that schema in a Single-Page-App, using Apollo and the React Profile for Grails. Please refer to the GORM for GraphQL Documentation, and related Grails & React Guides to learn more.