Over the span of the past few months I’ve been learning about GraphQL and how to use it as an alternative to RESTful API development. The focus thus far had been around GraphQL and Golang as well as GraphQL and Node.js. I might have squeeze out all I can on those two development languages so I’ve decided to switch gears and press my luck with some Java.
In this tutorial, we’re going to see how to quickly get up and running with GraphQL using Java and the popular Spring Boot framework.
Before we get into things, I want to give credit where credit is deserved. Prithviraj Pawar wrote a great article titled, How to get Your GraphQL Java Server Up and Running in no Time, which gave me a lot of ideas even though there were things to be desired, such as a database instead of mock data. I encourage you to read his article after reading through mine.
The Requirements
There are a few prerequisites that must be met before we jump into the GraphQL and development side of things.
We’re going to assume that you already have Couchbase installed, configured, and running. Anything after Couchbase Server 5.0 will do because we’re only going to be using basic features.
We’re also going to assume that you’ve downloaded a Spring Boot base project. The Spring Boot website has a great generator that lets you define the dependencies. If you want to stay consistent to my guide, you’ll need to create a Gradle project rather than Maven.
Defining the Gradle Project Dependencies
When you have a Spring Boot template in hand, it will likely be very bare. We need to include the dependencies we plan to use in our Gradle configuration.
Open the project’s build.gradle file and include the following:
1 2 3 4 5 6 |
dependencies { compile('org.springframework.boot:spring-boot-starter-web') compile('com.couchbase.client:java-client') compile('com.graphql-java:graphql-java:9.0') testCompile('org.springframework.boot:spring-boot-starter-test') } |
We’ll be using Spring Boot to serve our GraphQL endpoint, the Couchbase Java SDK for interactions with our database, and the GraphQL plugin for our query processing.
Bootstrapping the Java API with RESTful Endpoints
With the dependencies in place, we can start developing our API. Even though we’re using GraphQL as a replacement to all the common RESTful API stuff, it doesn’t mean that REST is completely out of the equation. We still need an endpoint to send our GraphQL queries to.
Depending on what you named your project, package, etc., open the file that contains your main
function and include the following:
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 |
package com.couchbase.graphql; import com.couchbase.client.java.Bucket; import com.couchbase.client.java.Cluster; import com.couchbase.client.java.CouchbaseCluster; import com.couchbase.client.java.document.json.JsonObject; import graphql.schema.DataFetcher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.GraphQL; import graphql.schema.GraphQLSchema; import graphql.schema.StaticDataFetcher; import graphql.schema.idl.RuntimeWiring; import graphql.schema.idl.SchemaGenerator; import graphql.schema.idl.SchemaParser; import graphql.schema.idl.TypeDefinitionRegistry; import javax.annotation.PostConstruct; import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring; import java.io.File; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; @SpringBootApplication @RestController public class GraphqlApplication { public static void main(String[] args) { SpringApplication.run(GraphqlApplication.class, args); } @RequestMapping(value="/", method= RequestMethod.GET) public Object rootEndpoint() { Map<String, Object> response = new HashMap<String, Object>(); response.put("message", "Hello World"); return response; } @RequestMapping(value="/graphql", method= RequestMethod.POST) public Object graphql(@RequestBody String request) { JsonObject jsonRequest = JsonObject.fromJson(request); return jsonRequest; } } |
Remember, your package and class name may not match mine. Fill in the gaps where appropriate.
You’ll notice that we have two endpoints, where one is completely optional. I created the rootEndpoint
as a way to test that my RESTful API was in fact working. The graphql
endpoint is where we’re going to be spending our time. It expects a POST request as well as a request body. The request body will eventually be an actual GraphQL query, but for now we can leave it as is.
Designing a GraphQL Schema Document
Up until now we’ve only done some basic Spring Boot preparation work and nothing really related to GraphQL. For GraphQL to be possible, we need to know a defined schema that explains the queries that are available as well as the types of data that we are working with. There are numerous ways to do this, but we’re going to focus on a schema document.
Within the resources directory of your project, include a schema.graphql file that contains the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
type Query{ pokemons: [Pokemon] games: [Game] game(id: String): Game } type Game { id: String name: String } type Pokemon { id: String game: Game name: String height: Int weight: Int } |
In case it isn’t obvious, we’re going to be working with Pokemon data. In the above file we’ve defined the queries that are available and the two data types that are available.
There are a variety of Pokemon games on the market and there are different Pokemon in each of the games, hence the relationship that we’ve defined. If you’re a true Pokemon fan you’ll know there is a little bit more to it than this, but for this example it is enough.
Developing the Fetching Logic for the Couchbase NoSQL Database
With the schema defined, we need to come up with database logic to be ran when a query is executed and that database logic must return results that match each of the two data types.
To make our lives a bit easier when it comes to project maintenance, we’re going to create a separate file for managing the database. Within your package, create a Database.java file that contains the following:
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 |
package com.couchbase.graphql; import com.couchbase.client.java.Bucket; import com.couchbase.client.java.document.JsonDocument; import com.couchbase.client.java.document.json.JsonObject; import com.couchbase.client.java.query.N1qlQuery; import com.couchbase.client.java.query.N1qlQueryResult; import com.couchbase.client.java.query.N1qlQueryRow; import graphql.schema.DataFetcher; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class Database { public static DataFetcher getPokemonData(Bucket bucket) { } public static DataFetcher getGameData(Bucket bucket) { } public static DataFetcher getGamesData(Bucket bucket) { } private static List<Map<String, Object>> extractResultOrThrow(N1qlQueryResult result) { } } |
For the most part, the above set of functions should look familiar because we’ve defined them somewhat to match what we had in our GraphQL schema file.
A few things to note before we start filling each of the functions:
- The bucket will be defined in our main project file shortly after we define our logic.
- GraphQL expects a certain type of response, hence the
extractResultOrThrow
wrapper function that we’ve created.
With that noted, let’s start by creating the wrapper function. The extractResultOrThrow
function will be used only on N1QL queries and it looks like the following:
1 2 3 4 5 6 7 |
private static List<Map<String, Object>> extractResultOrThrow(N1qlQueryResult result) { List<Map<String, Object>> content = new ArrayList<Map<String, Object>>(); for (N1qlQueryRow row : result) { content.add(row.value().toMap()); } return content; } |
Essentially we’re just looping through a N1QL result set and creating a List
of Map
from it.
Assuming that we have game data in our database, we can query for that data in the getGamesData
function:
1 2 3 4 5 6 7 8 9 10 |
public static DataFetcher getGamesData(Bucket bucket) { return environment -> { System.out.println("FETCHING GAMES DATA..."); String statement = "SELECT example.* " + "FROM example " + "WHERE type = 'game'"; N1qlQueryResult queryResult = bucket.query(N1qlQuery.simple(statement)); return extractResultOrThrow(queryResult); }; } |
Using N1QL we can query for documents that match the type
of game
and process the results with our wrapper function. If you really wanted to be strict, instead of using a wildcard asterisk, you could define the properties that match your schema.
The next function, getPokemonData
, will allow us to query for our Pokemon data in a similar fashion to how we did it with game data.
1 2 3 4 5 6 7 8 9 10 |
public static DataFetcher getPokemonData(Bucket bucket) { return environment -> { System.out.println("FETCHING POKEMON DATA..."); String statement = "SELECT example.* " + "FROM example " + "WHERE type = 'pokemon'"; N1qlQueryResult queryResult = bucket.query(N1qlQuery.simple(statement)); return extractResultOrThrow(queryResult); }; } |
You’ll notice that we didn’t do anything new beyond change the type
property. This is where things can get interesting. We have a data relationship for Pokemon in regards to game data. We could either fetch it in our current query or break it up to be more modular and potentially more light weight. We’re going to go with the modular and light weight option.
This brings us to our getGameData
function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public static DataFetcher getGameData(Bucket bucket) { return environment -> { HashMap<String, Object> parent = environment.getSource(); JsonDocument document; if(parent != null) { System.out.println("FETCHING GAME DATA FOR " + (String)parent.get("game") + "..."); document = bucket.get((String) parent.get("game")); } else { System.out.println("FETCHING GAME DATA FOR " + (String)environment.getArgument("id") + "..."); document = bucket.get((String)environment.getArgument("id")); } return document.content().toMap(); }; } |
There are two things happening in the above function, neither of which use N1QL.
In our schema we have defined an argument based query for game data as well as a data relationship in the Pokemon data. We’re going to accommodate both those scenarios in the same function.
Using the environment.getSource()
function we can see if there is any parent data that goes with the request. An example of this parent data might be when we query for Pokemon data. The game
field refers to the getGameData
function while the other data such as the name, weight, or other, refers to the parent data. In the database, the game information is stored as an id so after we extract it from the parent data, we can do a lookup for game data.
The other scenario is if we pass an argument that represents a game id.
Both are valid and both do different things. In the end, we only want a single result rather than an array of results.
Wiring the Application Together for Successful GraphQL Processing
We are in the home stretch now. We’ve got our API and our database logic in place. The final step is to wire the GraphQL schema to the database logic and watch the results in all their glory.
Remember in the previous step I mentioned configuring our bucket information? Let’s take care of that first. Open the project’s application.properties file found in the resources directory. Include the following:
1 2 3 4 |
hostname=127.0.0.1 bucket=example username=example password=123456 |
My database information is just an example. Make sure to replace it with the information that you’re using for your instance of Couchbase.
Back in your main project file, we can map the values and connect to our instance:
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 69 70 71 72 73 74 75 76 77 78 |
package com.couchbase.graphql; import com.couchbase.client.java.Bucket; import com.couchbase.client.java.Cluster; import com.couchbase.client.java.CouchbaseCluster; import com.couchbase.client.java.document.json.JsonObject; import graphql.schema.DataFetcher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.GraphQL; import graphql.schema.GraphQLSchema; import graphql.schema.StaticDataFetcher; import graphql.schema.idl.RuntimeWiring; import graphql.schema.idl.SchemaGenerator; import graphql.schema.idl.SchemaParser; import graphql.schema.idl.TypeDefinitionRegistry; import javax.annotation.PostConstruct; import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring; import java.io.File; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; @SpringBootApplication @RestController public class GraphqlApplication { @Value("${hostname}") private String hostname; @Value("${bucket}") private String bucket; @Value("${username}") private String username; @Value("${password}") private String password; public @Bean Cluster cluster() { Cluster cluster = CouchbaseCluster.create(this.hostname); cluster.authenticate(this.username, this.password); return cluster; } public @Bean Bucket bucket() { return cluster().openBucket(this.bucket); } private GraphQL build; public static void main(String[] args) { SpringApplication.run(GraphqlApplication.class, args); } @RequestMapping(value="/", method= RequestMethod.GET) public Object rootEndpoint() { } @RequestMapping(value="/graphql", method= RequestMethod.POST) public Object graphql(@RequestBody String request) { } } |
Notice that we’ve created two Bean
and used the information from our properties file. Just like that we’re connected to Couchbase.
The next step is to initialize our GraphQL schema. There are numerous ways to do this, but I found it easier using the Spring Boot annotation for when the application starts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@PostConstruct() public void init() { ClassLoader classLoader = getClass().getClassLoader(); File schemaFile = new File(classLoader.getResource("schema.graphql").getFile()); SchemaParser schemaParser = new SchemaParser(); TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schemaFile); RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() .type("Query",typeWiring -> typeWiring .dataFetcher("pokemons",Database.getPokemonData(bucket())) ) .type("Query",typeWiring -> typeWiring .dataFetcher("games",Database.getGamesData(bucket())) ) .type("Query",typeWiring -> typeWiring .dataFetcher("game",Database.getGameData(bucket())) ) .type("Pokemon",typeWiring -> typeWiring .dataFetcher("game",Database.getGameData(bucket())) ).build(); SchemaGenerator schemaGenerator = new SchemaGenerator(); GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); this.build = GraphQL.newGraphQL(graphQLSchema).build(); } |
The init
function will be called when the application starts. It will load our schema file from the resources directory and define wiring. For example, we know that pokemons
, games
, and game(id: String)
are all types of queries. Each get mapped to the appropriate database function and the open bucket is passed. Then we establish a mapping of game
to the Pokemon
type. We are more or less defining how fields and queries are tied to database interaction.
Once we build the schema we can tie it to our class variable. With the built schema, we can finalize our GraphQL RESTful endpoint:
1 2 3 4 5 6 7 |
@RequestMapping(value="/graphql", method= RequestMethod.POST) public Object graphql(@RequestBody String request) { JsonObject jsonRequest = JsonObject.fromJson(request); ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(jsonRequest.getString("query")).build(); ExecutionResult executionResult = this.build.execute(executionInput); return executionResult.toSpecification(); } |
In our endpoint we take the query string that was passed with the request and execute it with our built schema. The result will be whatever the client has asked for in the query.
So what does a query look like? Take the following for example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{ pokemons { name weight game { name } } games { name } game(id: "game-2") { name } } |
The above query is probably not something anyone would ever need to run, but you could if you wanted to. In the above example we are querying for all Pokemon data and the game they are a part of. We are also querying for all games as well as a specific game. All three parts of the query are unrelated, but exist in the same query.
As a cool fun fact, in the pokemons
query, if we choose not to query for game data, the database function is never called. GraphQL will only execute functions as they are needed.
Conclusion
You just saw how to build a GraphQL application using Spring Boot, Java, and Couchbase Server as the NoSQL database. As you saw in my sample query, we were able to query many different unrelated pieces of data in a single request, something that would have required several in a RESTful API.
For more information on using the Java SDK with Couchbase, check out the Couchbase Developer Portal.