Introduction to GraphQL - Part II
Aneshka Goyal
AWS Certified Solutions Architect | Software Development Engineer III at Egencia, An American Express Global Business Travel Company
Implementing a GraphQL Server Application
In the client server architecture, the client requests server for resources and the server has the responsibility of fulfilling those requests. It's no different here as well. GraphQL also needs a server that is able to get, parse and execute client requests and respond to them. Thus we would try to build one here.
GraphQL Server
We would be building a GraphQL server application that would take in client requests, retrieve the data and transform those into response that would then be sent to the client. We would be building an employee management system, in continuation of the first part of the series and would leverage the schema we defined here.
This would be a Spring boot + Java application, using maven for dependency management. First step would be to initialize the spring boot application using spring initializr . The POM file of our applications looks like below.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>graphql</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>graphql</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
<version>2.7.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>r05</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.graphql-java/graphql-spring-boot-starter -->
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>5.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
</project>
The dependency graphql-spring-boot-starter and spring-boot-starter-graphql fulfil all the requirements needed to build and run a GraphQL service i.e. intercept, execute and respond to the graphQL requests.
Lets now move towards the next step of creating a .graphql file for the schema named as schema.graphql we would be parsing this file to serve as a contract between clients and server. This is defined using GraphQL Schema Definition language which was discussed in the first part.
schema
query: Query
mutation: Mutation
}
type Mutation{
createEmp(input: EmployeeInput): Employee
deleteEmp(id:ID!):Employee
}
type Query {
empById(id: ID): Employee
}
input EmployeeInput{
name: String
dept: String
}
type Employee {
id: ID
name: String
dept: Department
}
type Department{
id: ID
name: String!
}
{
The schema defines the custom object types and special object types like query and mutation and what fields can be expected in these object types. The detail coverage of the SDL is in part I
We now would need to define resolvers as GraphQL resolves the queries(or mutations) at field level means we should have resolvers for each field that we define in our schema, so that the client can declare the field in the query/mutation and retrieve response. Also we discussed resolution in part I, now lets implement resolvers for fields in the schema and dive in a bit deep.
@Component
public class GraphQLDataFetchers {
private List<Map<String, Object>> employees = new LinkedList<>();
@PostConstruct
public void init(){
employees.add(getEmp("emp-1","Ane","dept-1"));
employees.add(getEmp("emp-2","Moby","dept-1"));
}
private List<Map<String, Object>> departments = Arrays.asList(
ImmutableMap.of("id", "dept-1",
"name", "HR"
),
ImmutableMap.of("id", "dept-2",
"name", "Tech"
)
);
public DataFetcher getEmployeeByIdDataFetcher() {
return dataFetchingEnvironment -> {
String empId = dataFetchingEnvironment.getArgument("id");
return employees
.stream()
.filter(employee -> employee.get("id").equals(empId))
.findFirst()
.orElse(null);
};
}
public DataFetcher getDepartmentDetailsDataFetcher() {
return dataFetchingEnvironment -> {
Map<String,Object> employee = dataFetchingEnvironment.getSource();
return departments
.stream()
.filter(dept -> dept.get("id").equals(employee.get("dept")))
.findFirst()
.orElse(null);
};
}
public DataFetcher setEmployee() {
return dataFetchingEnvironment -> {
Map<String,Object> input = dataFetchingEnvironment.getArgument("input");
Map<String,Object> emp = getEmp(UUID.randomUUID().toString(),input.get("name").toString(),input.get("dept").toString());
employees.add(emp);
return emp;
};
}
public DataFetcher deleteEmployee() {
return dataFetchingEnvironment -> {
String empId = dataFetchingEnvironment.getArgument("id");
Map<String,Object> employee = employees.stream().filter(emp -> emp.get("id").equals(empId)).findFirst().orElse(null);
employees = employees.stream().filter(emp -> !emp.get("id").equals(empId)).collect(Collectors.toList());
return employee;
};
}
private Map<String,Object> getEmp(String id, String name, String deptid){
Map<String, Object> emp = new HashMap<>();
emp.put("id",id);
emp.put("name",name);
emp.put("dept",deptid);
return emp;
}
}
So here we are creating a component called GraphQLDataFetchers, this has our in memory data source. As we are building an employee management system, we have employee map that is mutable and can undergo updations while the other map we have for departments, assuming that departments are fixed and hence represented by immutable map. It has all the resolvers for different fields in our schema like getEmployeeByIdDataFetcher is for resolving the empById entry point and also gets the the id from the environment as shown. Similarly we have one data fetcher (getDepartmentDetailsDataFetcher) created to resolve the department field and it gets the employee for which the department needs to be resolved from the environment itself. Thus the execution result of resolver at say L1 are passed to the resolvers at L2 as seen in case of department.
Apart from having data fetchers for queries this component also has fetchers for the mutations that we support. In this example we support creation of new employee records and deletion of existing records.
The next logical step that comes to our mind is to link these methods that we created in the component with our schema so that the GraphQL server knows on which query field which resolver needs to be invoked. This is done below by creating a configuration which parses the schema from the .graphql file that we created and also establishes the connection.
@Configuration
public class GraphQLConfig {
private GraphQL graphQL;
@Autowired
GraphQLDataFetchers graphQLDataFetchers;
@Bean
public GraphQL graphQL(){
return graphQL;
}
@PostConstruct
public void init() throws IOException {
URL url = GraphQLConfig.class.getClassLoader().getResource("schema.graphql");
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);
}
private RuntimeWiring buildWiring() {
Map<String, DataFetcher> map = new HashMap<>();
map.put("createEmp",graphQLDataFetchers.setEmployee());
map.put("deleteEmp",graphQLDataFetchers.deleteEmployee());
return RuntimeWiring.newRuntimeWiring()
.type(newTypeWiring("Query")
.dataFetcher("empById", graphQLDataFetchers.getEmployeeByIdDataFetcher()).build())
.type(newTypeWiring("Employee")
.dataFetcher("dept",graphQLDataFetchers.getDepartmentDetailsDataFetcher()).build())
.type(newTypeWiring("Mutation").dataFetchers(map).build())
.build();
}
}
Here we are creating a configuration to parse the schema.graphql file and building a schema by wiring the object types and field level resolvers for Employee, Department and special object types like Query and Mutation. One thing to note here is that we need not define the resolvers for scalar fields like Id etc of employee because the field is named same in the schema and data source and hence is taken care of and does not need explicit resolver binding.
Since we have everything in place now, next step we would create our POST endpoint, that would be one single endpoint to serve all types of requests as we know that unlike REST, GraphQL does not support multiple endpoints with different contracts. This can be done by having our controller defined as below.
领英推荐
@Controller
public class GraphQLController {
@Autowired
private GraphQL graphQL;
@PostMapping("gql")
@ResponseBody
public ResponseEntity<ExecutionResult> getEmployee(@RequestBody String query){
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("Access-Control-Allow-Origin",
"*");
return ResponseEntity.ok().headers(responseHeaders).body(graphQL.execute(query));
}
}
GraphQL 3 step approach to respond to a query.
The best part about this is that our application doesn't have to take care of these three steps these are taken care by the graphQL itself.
Another interesting thing is Introspection in GraphQL.
As a client I might want to know the schema supported by this server to know what all queries are supported and what all mutations are allowed.This is where introspection is used.This is supported by GraphQL by default and has a special syntax as the root is __schema, which is default by GraphQL and we can explore what all query types are supported and other information can be obtained about the object types defined and what values(scalar values) we can expect.
Following is an example of introspection run on our schema and how the response looks like. The below snippet is used to find the different types our schema has and what are their kinds like Objects, scalars, enum etc.
{
__schema {
? ? types {
? ? ? name
? ? ? kind
? ? }
? }
}
Response to the above looks something like below
? ?{
"errors": [],
? ? "data": {
? ? ? ? "__schema": {
? ? ? ? ? ? "types": [
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "Boolean",
? ? ? ? ? ? ? ? ? ? "kind": "SCALAR"
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "Department",
? ? ? ? ? ? ? ? ? ? "kind": "OBJECT"
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "Employee",
? ? ? ? ? ? ? ? ? ? "kind": "OBJECT"
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "EmployeeInput",
? ? ? ? ? ? ? ? ? ? "kind": "INPUT_OBJECT"
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "ID",
? ? ? ? ? ? ? ? ? ? "kind": "SCALAR"
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "Mutation",
? ? ? ? ? ? ? ? ? ? "kind": "OBJECT"
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "Query",
? ? ? ? ? ? ? ? ? ? "kind": "OBJECT"
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "String",
? ? ? ? ? ? ? ? ? ? "kind": "SCALAR"
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "__Directive",
? ? ? ? ? ? ? ? ? ? "kind": "OBJECT"
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "__DirectiveLocation",
? ? ? ? ? ? ? ? ? ? "kind": "ENUM"
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "__EnumValue",
? ? ? ? ? ? ? ? ? ? "kind": "OBJECT"
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "__Field",
? ? ? ? ? ? ? ? ? ? "kind": "OBJECT"
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "__InputValue",
? ? ? ? ? ? ? ? ? ? "kind": "OBJECT"
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "__Schema",
? ? ? ? ? ? ? ? ? ? "kind": "OBJECT"
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "__Type",
? ? ? ? ? ? ? ? ? ? "kind": "OBJECT"
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? "name": "__TypeKind",
? ? ? ? ? ? ? ? ? ? "kind": "ENUM"
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ]
? ? ? ? }
? ? },
? ? "extensions": null,
? ? "dataPresent": true
}
All the names with __(double underscore) are defined by GraphQL internally. Similarly introspection can be used to get what all types are defined and their insights.
Thus here we built a graphQL server application in sync with the schema and CRUD operations that we defined in part I of the series. We actually saw the various parts in action.
What's next
Sources of knowledge