Introduction to GraphQL - Part II

Introduction to GraphQL - Part II

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.

  1. Parsing - Convert the query string to an abstract syntax tree, if the syntax is not correct return error to the client.
  2. Validation - It can be that syntactically the query submitted by the client is correct but semantically when compared against our schema it makes little sense as the resolvers for a particular field are not present, in that case also error needs to be returned to the client.
  3. Execution - If the above 2 steps are complete without encountering any errors, then the actual execution begins wherein the resolvers for each field at one level are called concurrently and once all of these have returned response we move to the next level.

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

  • Building a GraphQL client - As once the server is ready we want to consume the information sent across.

Sources of knowledge

  • https://graphql.org/learn/introspection/
  • https://www.graphql-java.com/documentation/data-fetching/



要查看或添加评论,请登录

Aneshka Goyal的更多文章

  • Introduction to Distributed Tracing

    Introduction to Distributed Tracing

    What is Distributed Tracing? The word tracing is to trace the request as it flows through the system. Since modern…

    2 条评论
  • Introduction to Service Discovery

    Introduction to Service Discovery

    What is Service Discovery? Service Discovery as the name suggests allows us to know or discover where each instance of…

  • Introduction to Micro frontend

    Introduction to Micro frontend

    What is Micro frontend? The term “micro frontends” debuted in the 2016 ThoughtWorks Technology Radar guide. At its…

  • Introduction to Pub-Sub and Streams with Redis&SpringBoot

    Introduction to Pub-Sub and Streams with Redis&SpringBoot

    Publish/Subscribe Problem: Let's say we have synchronous messaging between two components of our system called as…

    2 条评论
  • Introduction to Time Series Database - InfuxDB

    Introduction to Time Series Database - InfuxDB

    What is Time Series Data? As the title of the blog depicts we would be discussing about time series databases and in…

    1 条评论
  • Introduction to Ontology

    Introduction to Ontology

    What is Ontology? An ontology is a formal and structural description of knowledge about a specific domain. Knowledge is…

  • From Java 17 to Java 21 - Features and Benefits

    From Java 17 to Java 21 - Features and Benefits

    Java has been constantly evolving with new features and enhancements. With the recent LTS (Long term support) version…

    2 条评论
  • Vault Authentication and Springboot integration

    Vault Authentication and Springboot integration

    What is Vault? Vault is an identity-based secrets and encryption management system. A secret is anything that we want…

  • Introduction to gRPC with Spring boot

    Introduction to gRPC with Spring boot

    Overview RPC stands for remote procedure calls. In this the client is able to directly invoke a method on server…

    6 条评论
  • Introduction to Triple Crown

    Introduction to Triple Crown

    Organizations are always trying to improve how they work, in order to increase efficiency and reduce errors. This…

    4 条评论

社区洞察

其他会员也浏览了