Highly optimized APIs Blog

Integrate GraphQL into Serverless Architectures

Michael Dähnert
Sep 21, 2021

After several years on the market, GraphQL is now a mature and established alternative to REST. You should consider it when creating or further developing an API. Many applications such as Facebook, Instagram, and XING already successfully use this REST alternative [1]. That’s a good reason to provide insights into how GraphQL can be integrated into modern serverless architectures, with minimum effort. A highly scalable implementation is presented in the interaction between GraphQL with AWS Lambda. It can be adapted to various architectures and frameworks.

As a frontend developer, you’ve likely become annoyed when a REST call didn’t deliver all the data that you need. Even backend developers are often repeatedly asked by their colleagues to add another property to a response when it was missing. Thanks to GraphQL, these problems are in the past. Although REST defines fixed structures for the return of a call, GraphQL only delivers data needed in the frontend. It avoids over- and underfetching since the call to the interface names the method to be executed as well as the return structures.

Modern developments in microservices and serverless architectures make it possible to create highly scalable systems. When combined with GraphQL for netload-optimized APIs, you’ll receive highly optimized, data-driven systems. The article will give you a glimpse into GraphQL. Specifically, we will discuss the interaction with AWS Lambda as a representative of serverless architectures.

GraphQL Schema: Important Terms


GraphQL provides a number of terms used in its schema definition. This article will cover some of them. Please refer to the GraphQL documentation for the rest [2]:

Query – Read accesses to data

Mutation – Write access to data; the structure of a mutation within the schema corresponds to a query, but begins with the word “mutation”

Inline Fragments – Object trees can be structured cleanly and reused in other queries, avoiding duplicate code

Type and InputType – Objects and their properties are defined in the schema. This info is known to the client and server, so validation can be used directly when starting the server and executing a request

Scalar – Objects like date values (DateTime) can be added to GraphQL native elements like String, Int, and Boolean and are used directly as a data type

Argument/Variable – When passing server requests, arguments can be written directly into the request or passed as separate variables

Required fields – Described within the schema by putting a “!” after

Directive – Desired return structures can be filtered by the conditional operators if and skip

An initial overview

So what exactly does the call to a GraphQL server look like? The client creates a JSON request with the elements query and variables. The content of the query object is a string that contains the Graph Query Language as a value. Normal JSON objects of varying complexity are passed as variables. This query is sent to the server via a classic POST request. For example, the endpoint is /graphql. Listing 1 shows a server request including parameters.

{
  "query": "
    query testQuery($id: Int!) {
      getCustomer(id: $id) {
        id
        name
        orders {
          date
        }
      }
    }
  ",
  "variables": {
    "id": 0
  }
}

STAY UP TO DATE!

Learn more about API Conference

This can now be extended as you wish. For example, you can add the customer’s birthday, their order IDs, or last log-in date. Everything is possible as long as the properties are defined as returns within GraphQL. This is achieved with a schema (Listing 2). It contains all operations and object structures that GraphQL will work with (Box: “GraphQL Schema: Important Terms”).

type Query {
  getCustomer(id: Int!) : Customer
}
 
type Customer {
  id: Int!
  name: String!
  age: Int
  birthdate: String
 
  orders: [Order]
}
 
type Order {
  amount: Int!
  date: String
}

After the request is processed on the servicer, it returns the response. This will also be in JSON format, so it can be read and processed by existing client  implementations (Listing 3).

{
  "data": {
    "getCustomer": {
      "id": 0,
      "name": "Micha",
      "orders": [
        {"date": "2017-12-21"}, {"date": "2018-02-17"}, {"date": "2018-02-21"}
      ]
    }
  }
}

Integration as Java Backend

Now that you know about the basic uses of a GraphQL server, let’s move on to implementation. Next, we will create an AWS serverless (Lambda) function with a connection to a NoSQL database. Since AWS supports programming languages such as Node.js, Python, and Java, first we must choose our programming language. The following example uses Java 8 in conjunction with AWS’s in-house NoSQL database DynamoDB. Here, it’s enough to create a Maven project with the following AWS and GraphQL dependencies (Listing 4) [3].

<dependencies>
  <!-- GraphQL dependencies -->
  <dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java</artifactId>
    <version>7.0</version>
  </dependency>
  <dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>4.3.0</version>
  </dependency>
 
  <!-- AWS dependencies -->
  <dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-lambda-java-core</artifactId>
    <version>1.2.0</version>
  </dependency>
  <dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-dynamodb</artifactId>
    <version>1.11.280</version>
  </dependency>
  <dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.2</version>
  </dependency>
</dependencies>

The endpoint for calls is a method with two parameters [4], the Input parameter and the Context parameter:

public String handleRequest(InputType input, Context context)

The Input parameter has already been optimized for GraphQL. AWS Lambda automatically deserializes the received JSON request into the named objects. For GraphQL, the following two properties will be sufficient:

class InputType {

  String query;

  Map<String, Object> variables;

  …

}

Starting up the serverless function

When starting the Lambda function, the GraphQL schema is initially analyzed, and corresponding Java handlers are wired. In order to do this, the schema must be filled with the necessary information when it is created. There are three important aspects here:

First, the parsing and validation of the schema; this detects syntax errors that already exist during the startup process:

SchemaParserBuilder parser = SchemaParser.newParser().file(“schema.graphqls”);

Second, setting up the Java resolvers; these classes contain the later business logic:

parser.resolvers(new QueryResolver());

GraphQLSchema schema = parser.build().makeExecutableSchema();

Third, passing the data to the GraphQL service – the passed parameters are parsed by GraphQL and the corresponding business logic is called:

ExecutionInput exec = ExecutionInput.newExecutionInput()

  .query(input.getQuery())

  .variables(input.getVariables())

  .build();

The results can directly be used later as a response.

return GraphQL.newGraphQL(schema).build()

  .execute(exec)

  .toString();

Requests workflow

After the query has been passed to GraphQL, the method to be called is parsed and determined. Additionally, the passed parameters are automatically validated and converted into the corresponding Java objects. The GraphQL service has done its job. Now, all Java functionality can be executed in a known form. Listing 5 connects to a DynamoDB table and reads a customer object. What’s special here is: If the Lambda function and the created DynamoDB table are operated in an AWS account, then it’s enough to just specify the AWS region and table name as connection parameters.

public class QueryResolver implements GraphQLQueryResolver {
 
  public Customer getCustomer(int id) {
    return getDB().load(Customer.class, id);
  }
 
  private DynamoDBMapper getDB() {
    AmazonDynamoDBClientBuilder builder = AmazonDynamoDBClientBuilder.standard();
    builder.withRegion(Regions.EU_CENTRAL_1);
 
    return new DynamoDBMapper(builder.build());
  }
 
}

When working with DynamoDB objects, you can proceed in the usual POJO manner (Listing 6). For this purpose, AWS offers annotations based on JPA that convert return values into Java objects when communicating with the database.

@DynamoDBTable(tableName = "customer")
public class Customer {
  @DynamoDBHashKey(attributeName = "id")
  public Integer getId() { return id; }
 
  @DynamoDBAttribute(attributeName="name")
  public String getName() { return name; }
 
  @DynamoDBAttribute(attributeName="orders")
  public List&amp;lt;Order&amp;gt; getOrders() { return orders; }
 
  [...]
}
 
 
@DynamoDBDocument
public class Order {
  @DynamoDBHashKey(attributeName = "id")
  public Integer getId() { return id; }
 
  @DynamoDBHashKey(attributeName = "date")
  public String getDate() { return date; }
 
  [...]
}

As soon as the processing of the request is finished, the results are passed directly to the executing GraphQL service. Now, GraphQL’s added value comes into play. You will recall that the request only asked for the customer’s ID, name, and order. But the Customer object contains additional properties. Now, if this object is passed, the GraphQL service removed the unwanted properties and ignores structures. This way, the client only receives the requested elements.

We should briefly mention write queries (mutations). These run according to the same scheme as queries, and are marked in the query with the keyword “mutation”. Function extensions require only three updates in GraphQL.

First, the extended schema:

type Mutation {

  addOrder(newOrder: OrderInput!) : Order

}

 

input OrderInput {

  customerId: Int!

  amount: Int!

}

Second, register handler:

parser.resolvers(new MutationResolver());

Third, implement business logic:

public Order addOrder(OrderInput newOrder) {

  Customer c = getDB().load(Customer.class, newOrder.getCustomerId());

  Order o = new Order();

 

  o.setAmount(newOrder.getAmount());

  o.setDate(DateTime.now().toDateTimeISO().toString());

 

  c.getOrders().add(o);

  getDB().save(c);

 

  return o;

}

Table 1 shows the example query and return data. You can see from the table that the mutation’s structure is very similar to a query.

Request Response
{

  “query”: “

    mutation testMutation($orderInput: OrderInput!) {

      addOrder(newOrder: $orderInput) {

        amount

        date

      }

    }

  “,

  “variables”: {

    “orderInput”: {

      “customerId”: 0,

      “amount”: 123

    }

  }

}

{

  “data”: {

    “addOrder”: {

      “amount”: 123,

      “date”: null

    }

  }

}

Table 1: Example mutation: Request and response

STAY UP TO DATE!

Learn more about API Conference

Deploying and setting up AWS Lambda

There’s still one small detail missing before the Lambda function can run in AWS. The Maven project needs to be compiled as a fat JAR. For this, Maven gives you the possibility to work via the Shade plug-in. It bundles all required dependencies into a single artifact deployable to AWS [5]. By running mvn clean package, the artifact is created and can be uploaded to AWS. Now, Lambda is ready to work. If it has to work with a client such as Angular, then it must be connected to the Internet via an API Gateway and given the necessary roles and permission. Now, we will refer to the detailed AWS documentation on creating an API Gateway with proxy integration of a Lambda [6].

Summary and Conclusion

The example we looked at here is a good entry point into using GraphQL in Java. Implementing it within a serverless application guarantees highly scalable usage. In GraphQL, multiple methods can be bundled in a single request. There are also already-existing libraries that allow integration of a GraphQL interface seamlessly with Spring Boot, and offer various implementations for Angular, Node.js, Python, and more [7]. This guarantees seamless usage in server and client applications.

Thanks to its design, GraphQL is easy to integrate into existing architectures. Since it is only a wrapper between requested data and business logic, it’s easy to connect well-known databases like MySQL and Oracle using JPA. Because of its flexibility, it can be well-integrated alongside existing REST APIs to guarantee highly optimized requests to backend services.I will recommend two more links for interested readers. First, the democode accompanying this article is available on GitHub at https://github.com/mdaehnert/graphql-serverless-demo. I also highly recommend the GraphQL homepage, which presents many more ideas for using GraphQL: http://graphql.org/.

 

Links & Literature

[1] GraphQL users: https://stackshare.io/graphql/in-stacks 

[2] The GraphQL Documentation: http://graphql.org/learn/ 

[3] GraphQL and Maven: https://github.com/graphql-java 

[4] Java Programming with Lambda: https://docs.aws.amazon.com/lambda/latest/dg/java-programming-model.html 

[5] The Maven Shade Plug-in: https://maven.apache.org/plugins/maven-shade-plugin/usage.html 

[6] APIs as a Proxy for Lambda: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-lambda.html 

[7] Implementations for GraphQL: http://graphql.org/code/

Top Articles About Blog

All News & Updates of API Conference:

Behind the Tracks

API Management

A detailed look at the development of APIs

API Development

Architecture of APIs and API systems

API Design

From policies and identities to monitoring

API Platforms & Business

Web APIs for a larger audience & API platforms related to SaaS

ALL NEWS ABOUT THE API CONFERENCE!