GraphQL advertises itself as a query language for APIs. To understand what this means, let’s compare it with REST.
In REST, we usually have endpoints that give us information about a resource, for example: GET /user/1
could return something like:
1
2
3
4
5
{
"id": 1,
"name": "Jose",
"phone": "999-888-7777"
}
We have multiple endpoints we can use to get different information about different resources. If we need an endpoint that contains information from different resources, we sometimes create endpoints for specific purposes: GET /user/1?includeParents=true
. Which could return something like this:
1
2
3
4
5
6
7
8
9
{
"id": 1,
"name": "Jose",
"phone": "999-888-7777"
"parents": {
"mom": "Maria",
"dad": "Carlos"
}
}
For each endpoint, we usually create a route in our server and write the code necessary to get the information we need.
Often the code consists mostly of making calls to other services and stitching the data together. GraphQL provides a framework that can make these tasks easier for us.
GraphQL Schema
A GraphQL server is defined using a schema file. In this file, we define the types our service supports and queries that are possible against these types.
A GraphQL schema file looks like this:
1
2
3
4
5
6
7
8
9
type Query {
people: [Person]
}
type Person {
id: ID
name: String
phone: String
}
The Query
type is special in that it’s mandatory and it defines the entry point for GraphQL queries. By defining a people
field of type [Person]
in Query
we are saying that the people
query returns a list of Person
s.
The Person
type is defined by 3 fields that use scalar GraphQL types. The available scalar types are: Int
, Float
, String
, Boolean
and ID
(ID
is serialized the same way as String
).
Now that we know how to define our API, let’s build a server.
Starting a GraphQL server
There are many libraries that can help us start a GraphQL server. In this article, I’m going to show how to do it with Java and Spring using graphql-java.
I’m going to build my project with Bazel, so you might find my building a Spring Boot server with Bazel article useful to get you started.
We are going to need a folder with this structure:
1
2
3
4
5
6
7
8
9
10
11
12
├── BUILD
├── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── demo
│ │ ├── DemoApplication.java
│ │ └── GraphQLProvider.java
│ └── resources
│ └── schema.graphqls
└── WORKSPACE
Our WORKSPACE
file installs the required GraphQL and Spring artifacts:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
load('@bazel_tools//tools/build_defs/repo:http.bzl', 'http_archive')
RULES_JVM_EXTERNAL_TAG = '4.1'
RULES_JVM_EXTERNAL_SHA = 'f36441aa876c4f6427bfb2d1f2d723b48e9d930b62662bf723ddfb8fc80f0140'
http_archive(
name = 'rules_jvm_external',
strip_prefix = 'rules_jvm_external-%s' % RULES_JVM_EXTERNAL_TAG,
sha256 = RULES_JVM_EXTERNAL_SHA,
url = 'https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip' % RULES_JVM_EXTERNAL_TAG,
)
load('@rules_jvm_external//:defs.bzl', 'maven_install')
maven_install(
artifacts = [
'com.google.guava:guava:26.0-jre',
'com.graphql-java:graphql-java-spring-boot-starter-webmvc:1.0',
'com.graphql-java:graphql-java:11.0',
'javax.annotation:javax.annotation-api:1.3.2',
'org.springframework.boot:spring-boot-autoconfigure:2.1.3.RELEASE',
'org.springframework.boot:spring-boot-starter-web:2.1.3.RELEASE',
'org.springframework.boot:spring-boot:2.1.3.RELEASE',
'org.springframework:spring-beans:5.1.5.RELEASE',
'org.springframework:spring-context:5.1.5.RELEASE',
'org.springframework:spring-web:5.1.5.RELEASE',
],
repositories = [
'https://repo1.maven.org/maven2',
],
fetch_sources = True,
)
Our BUILD
file uses those dependencies to build our server:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java_binary(
name = 'app',
main_class = 'com.example.demo.DemoApplication',
srcs = glob(['src/**/*.java']),
resources = glob(["src/main/resources/**/*"]),
deps = [
'@maven//:com_google_guava_guava',
'@maven//:com_graphql_java_graphql_java',
'@maven//:com_graphql_java_graphql_java_spring_boot_starter_webmvc',
'@maven//:javax_annotation_javax_annotation_api',
'@maven//:org_springframework_boot_spring_boot',
'@maven//:org_springframework_boot_spring_boot_autoconfigure',
'@maven//:org_springframework_boot_spring_boot_starter_web',
'@maven//:org_springframework_spring_beans',
'@maven//:org_springframework_spring_context',
'@maven//:org_springframework_spring_web',
],
)
We put our schema definition in schema.graphqls
:
1
2
3
4
5
6
7
8
9
type Query {
people: [Person]
}
type Person {
id: ID
name: String
phone: String
}
DemoApplication.java
just takes care of initializing our Spring Boot app:
1
2
3
4
5
6
7
8
9
10
11
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
And finally, the magic happens in GraphQLProvider.java
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.example.demo;
import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Resources;
import graphql.GraphQL;
import graphql.schema.DataFetcher;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Component
public class GraphQLProvider {
private static final List<Map<String, String>> people = Arrays.asList(
ImmutableMap.of("id", "1",
"name", "Carlos",
"phone", "111-111-1111"),
ImmutableMap.of("id", "2",
"name", "Jose",
"phone", "999-999-9999")
);
private GraphQL graphQL;
@Bean
public GraphQL graphQL() {
return graphQL;
}
@PostConstruct
public void init() throws IOException {
URL url = Resources.getResource("schema.graphqls");
String sdl = Resources.toString(url, Charsets.UTF_8);
GraphQLSchema graphQLSchema = buildSchema(sdl);
this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
}
private GraphQLSchema buildSchema(String sdl) {
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
RuntimeWiring runtimeWiring = buildWiring();
SchemaGenerator schemaGenerator = new SchemaGenerator();
return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
}
public DataFetcher getPeopleDataFetcher() {
return dataFetchingEnvironment -> {
return people;
};
}
private RuntimeWiring buildWiring() {
return RuntimeWiring.newRuntimeWiring()
.type(newTypeWiring("Query").dataFetcher("people", getPeopleDataFetcher()))
.build();
}
}
We can run the application using:
1
bazel run :app
And we can test it using curl:
1
2
3
4
5
curl -g \
-X POST \
-H "Content-Type: application/json" \
-d '{"query":"{people{id name}}"}' \
http://localhost:8080/graphql
The result will be:
1
{"data":{"people":[{"id":"1","name":"Carlos"},{"id":"2","name":"Jose"}]}}
We’ll talk a little more about how to query data later in this article. Before that, we’re going to take a closer look into the code we just ran.
The magic starts in the init
method:
1
2
3
4
5
6
7
@PostConstruct
public void init() throws IOException {
URL url = Resources.getResource("schema.graphqls");
String sdl = Resources.toString(url, Charsets.UTF_8);
GraphQLSchema graphQLSchema = buildSchema(sdl);
this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
}
It starts by loading the schema.graphqls
file into a string. This string is used to build a our GraphQL schema and intialize our GraphQL server.
The buildSchema
method ties the schema and the corresponding wiring together:
1
2
3
4
5
6
private GraphQLSchema buildSchema(String sdl) {
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
RuntimeWiring runtimeWiring = buildWiring();
SchemaGenerator schemaGenerator = new SchemaGenerator();
return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
}
Wiring in this context refers to matching the different GraphQL queries to their implementation. This mapping is done in buildWiring
:
1
2
3
4
5
private RuntimeWiring buildWiring() {
return RuntimeWiring.newRuntimeWiring()
.type(newTypeWiring("Query").dataFetcher("people", getPeopleDataFetcher()))
.build();
}
We can see that this method matches the people
query with getPeopleDataFetcher
, which returns the DataFetcher
for getting all people
.
In this example we are not using any database, so the data fetcher consists of just returning a List
of Map
s that represent the fields of a Person
.
1
2
3
4
5
public DataFetcher getPeopleDataFetcher() {
return dataFetchingEnvironment -> {
return people;
};
}
Querying
We performed one query in the previous section using curl:
1
2
3
4
5
curl -g \
-X POST \
-H "Content-Type: application/json" \
-d '{"query":"{people{id name}}"}' \
http://localhost:8080/graphql
We are going to focus on the data we are sending to the /graphql
endpoint:
1
{"query":"{people{id name}}"}'
If we look closely, we’ll notice that it’s a JSON with the key query
and the value {people{id name}}
. The query
key identifies the query we are making. The value is the GraphQL query. We can see our query here with some formatting to make it easier to read:
1
2
3
4
5
6
{
people {
id
name
}
}
people
is the name of the operation we are performing, as defined in our GraphQL schema:
1
2
3
type Query {
people: [Person]
}
Since the people operation returns a list of Person
([Person]
), we can also specify exactly which fields we want. In the example above we retrieve id
and name
, but we could have retrieved any combination of the available fields.
Let’s add the ability to get a person by id
. First we add the operation to the schema:
1
2
3
4
5
6
7
8
9
10
type Query {
person(id: ID): Person
people: [Person]
}
type Person {
id: ID
name: String
phone: String
}
We added the person(id: ID): Person
line. The (id: ID)
part tells us that this operation accepts an id
argument.
This is what we need to add to our server to support this operation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
public DataFetcher getPersonDataFetcher() {
return dataFetchingEnvironment -> {
final String personId = dataFetchingEnvironment.getArgument("id");
return people
.stream()
.filter(person -> person.get("id").equals(personId))
.findFirst()
.orElse(null);
};
}
private RuntimeWiring buildWiring() {
return RuntimeWiring.newRuntimeWiring()
.type(newTypeWiring("Query").dataFetcher("person", getPersonDataFetcher()))
.type(newTypeWiring("Query").dataFetcher("people", getPeopleDataFetcher()))
.build();
}
...
To use this new operation, we can use this query:
1
2
3
4
5
6
{
person(id: "1") {
name
phone
}
}
This basically means: Give me the name
and phone
for person with id
equal to 1
.
Mutations
We have learned how to get data, but we also need to modify data. Let’s add an updatePerson
operation to our schema:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Query {
person(id: ID): Person
people: [Person]
}
type Mutation {
updatePerson(input: PersonInput!): Person
}
type Person {
id: ID
name: String
phone: String
}
input PersonInput {
id: ID!
name: String!
phone: String!
}
There are a few things to notice here. First of all, our mutation operation is inside the Mutation
type, as opposed to the read operations, which are under the Query
type.
The definition of the updatePerson
operation, is very similar to the one for reading a person
. One small difference is that we have an exclamation mark (!
) after the name of our argument. This simply means that the argument is mandatory.
Another important difference is that PersonInput
is not a type
, but an Input
. This is necessary for mutation operations.
Making a mutation request is also a little different. We can use this curl command:
1
2
3
4
5
curl -g \
-X POST \
-H "Content-Type: application/json" \
-d '{"query":"mutation {updatePerson(input: {id: \"1\", name: \"beto\", phone: \"123-456-7890\"}){name phone}}"}' \
http://localhost:8080/graphql
Even though it’s a mutation, we still need to use the query
key in our json. To distinguish a mutation from a read, we prefix our query with the mutation
keyword. Our mutation query looks like this:
1
2
3
4
5
6
7
8
9
10
mutation {
updatePerson(input: {
id: "1",
name: "beto",
phone: "123-456-7890"
}) {
name
phone
}
}
We also need update our code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
...
// Since we want to be able to edit the data, we stop using Immutables
private static final List<Map<String, String>> people;
static {
final Map<String, String> carlos = new HashMap<>();
carlos.put("id", "1");
carlos.put("name", "Carlos");
carlos.put("phone", "111-111-1111");
final Map<String, String> jose = new HashMap<>();
jose.put("id", "2");
jose.put("name", "Jose");
jose.put("phone", "222-222-2222");
people = Arrays.asList(carlos, jose);
}
...
public DataFetcher updatePersonDataFetcher() {
return dataFetchingEnvironment -> {
// Types are passed as maps
final Map<String, String> newPerson = dataFetchingEnvironment.getArgument("input");
// Find the person
final Map<String, String> currentPerson = people
.stream()
.filter(person -> person.get("id").equals(newPerson.get("id")))
.findFirst()
.orElse(null);
// This endpoint is just for editing. If the person is not found, do nothing
if (currentPerson == null) {
return null;
}
// Modify the person
currentPerson.put("name", newPerson.get("name"));
currentPerson.put("phone", newPerson.get("phone"));
return currentPerson;
};
}
private RuntimeWiring buildWiring() {
return RuntimeWiring.newRuntimeWiring()
.type(newTypeWiring("Query").dataFetcher("person", getPersonDataFetcher()))
.type(newTypeWiring("Query").dataFetcher("people", getPeopleDataFetcher()))
.type(newTypeWiring("Mutation").dataFetcher("updatePerson", updatePersonDataFetcher()))
.build();
}
...
We can see that registering a mutation is very similar to a query. We only need to use the Mutation
type wiring:
1
.type(newTypeWiring("Mutation").dataFetcher("updatePerson", updatePersonDataFetcher()))
Our data fetcher does the data update and then returns the new value as with any other query operation.
Conclusion
In this article we learned how to set up a GraphQL server and make simple queries and mutations.
There is a lot of functionality that wasn’t covered, but this should help you get started so you can try examples in the official documentation.
architecture
java
programming
server
]