Introduction
In this tutorial we will build a Pokemon API that consumes data from a Postgres database, with a simple endpoint that performs a search by id.
The final code is in this Github repository.
Postgres setup
If you already have Postgres installed locally, you can skip this part, otherwise the easiest way to do it is by running a Docker image. Just install Docker and then:
docker run -p5432:5432 -d postgres:11.4-alpine
This command will start a Postgres instance on port 5432 with default user postgres
and default database postgres
.
Spring Boot setup
We will start by creating the initial project files using Spring Initializr. I've selected:
- Gradle
- Java
- Spring Boot 2.1.6
- Spring Web Starter
- Spring Data JPA
- PostgreSQL Driver
Besides Spring dependencies, we need to add the GraphQL libraries:
- GraphQL Spring Boot Starter: will automatically create an
/graphql
endpoint - GraphQL Spring Boot Starter Test: for our unit tests
- GraphQL Java Tools: from its own documentation: "maps fields on your GraphQL objects to methods and properties on your java objects". This library requires version
1.3.*
of Kotlin, so you need to create agradle.properties
file on the project root directory with content:
kotlin.version=1.3.10
Database connection
After adding the dependencies, you can edit the src/main/resources/application.properties
file to add the Postgres configuration. If you are using the Docker command above to start Postgres locally, your file should be like this:
## PostgreSQL
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=
#drop n create table again, good for testing, comment this in production
spring.jpa.hibernate.ddl-auto=create-drop
Run your application to test if everything is working so far: ./gradlew bootRun
.
GraphQL Schema
GraphQL has a great schema language that adds type declatarations to its request and return values and couples this to the API implementation. Which means that what you declare on the schema must be implemented.
If we want to add an endpoint to search a pokemon by its id we should declare on src/main/resources/schema.graphqls
file:
type Pokemon {
id: ID!
name: String!
}
type Query {
pokemon(id: ID!): Pokemon
}
Our next step now must be the database search of a Pokemon instance by its id, or else the application won't run.
Query resolver
The declared schema expects to returns a Pokemon type
that contains required attributes id
and name
.
To our application, that means Pokemon
is a Java class with id
and name
properties but also a database table. We can use javax.persistence
annotations to automatically map Pokemon to database table with columns id
and name
:
@Entity
@Table(name = "pokemon")
public class Pokemon {
public Pokemon(final Long id, final String name) {
this.id = id;
this.name = name;
}
@Id
public Long id;
@Column
public String name;
}
The other expected class should be a Spring Bean
that implements GraphQLQueryResolver
interface and should have a method with name getPokemon
, that matches the parameters and response exactly like we defined in the scheme:
@Component
public class Query implements GraphQLQueryResolver {
public Pokemon getPokemon(Long id) {
return new Pokemon(1L, "Pikachu");
}
}
We can now perform an request at our new endpoint to check if its response is our Pikachu.
GraphiQL
GraphiQL configures an endpoint at our API that allow us to test any query. In our project it will run on address http://localhost:8080/graphiql
.
The left column is where we should write the queries, and the right column is the results. For example, if we enter the query:
# Searches a Pokemon with id 25 and returns its field 'name'
query {
pokemon(id: 25){
name
}
}
We should expect the result on right column:
{
"data": {
"pokemon": {
"name": "Pikachu"
}
}
}
So far it doesn't matter which parameter id
we pass because we've fixed the response object, but now we will implement a database search.
Fetch Pokemons from database
Currently our application is not doing a real database search but returning a fixed instance. Let's now implement this part.
First we create a PokemonRepository
interface that extends JpaRepository
:
@Repository
public interface PokemonRepository extends JpaRepository<Pokemon, Long> {
}
Then we change our Query
class to autowire this bean and perform the real database fetch:
@Component
public class Query implements GraphQLQueryResolver {
@Autowired
private PokemonRepository repository;
public Pokemon getPokemon(Long id) {
// Not returning a fixed instance anymore
return repository.findById(id).orElse(null);
}
}
Unit test
Our automated test will make use of GraphQLTestTemplate
class which allow us to enter a query
and verify its response. For example, if we want to test the search pokemon by id query, we first have to create a file in src/test/resources
with this query:
# src/test/resources/get-pokemon-by-id.graphql
query {
pokemon(id: "1") {
id
name
}
}
The test class should be annotated with @GraphQLTest
so it can resolve the GraphQLTestTemplate
instance, and PokemonRepository
should be annotated with @MockBean
so we can mock its response using Mockito
.
@RunWith(SpringRunner.class)
@GraphQLTest
public class DemoApplicationTests {
@Autowired
private GraphQLTestTemplate graphQLTestTemplate;
@MockBean
private PokemonRepository pokemonRepository;
@Test
public void getById() throws IOException {
Pokemon pokemon = new Pokemon(1L, "Pikachu");
when(pokemonRepository.findById(any()))
.thenReturn(Optional.of(pokemon));
GraphQLResponse response =
graphQLTestTemplate.postForResource("get-pokemon-by-id.graphql");
assertTrue(response.isOk());
assertEquals("1", response.get("$.data.pokemon.id"));
assertEquals("Pikachu", response.get("$.data.pokemon.name"));
}
}
Basically the scenario we are testing here is the following:
- Given the repository returns a pikachu when called the
findById
method - When we query GraphQL Api with
get-pokemon-by-id.graphql
- Then we expect the response to be a JSON containing the pikachu from repository
Conclusion
The challenge of implementing a GraphQL Api using Spring Boot relies mostly in the configuration and small details of Spring Boot functionality. Overall I think the integration works very well, specially the GraphQL Java Tools that enforces the code implementation.