REST API Projection — GraphQL like approach to solve Overfetching.

Vinodh Subramanian
8 min readDec 9, 2021

REST had become a prominent standard in designing web APIs over the past decade. Though REST API’s provides great feature support like statelessness, cacheability and structured access to resources, it also lacks in flexibility and efficiency in handling rapidly changing requirements of the client.

The most common problems that APIs developed based on REST faces are overfetching, underfetching and n+1. Overfetching is basically when a client is provided with more information that it’s actually required by the API it’s consuming. Underfetching and n+1 problems generally happen when a client had to make multiple API requests to fetch the complete information it requires. It’s very difficult to design the API in a way that it’s able to provide clients with their exact data needs. But there is always a way to solve a problem right. GraphQL to the rescue.

How GraphQL solves this?

What is Graphql ? GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools. — Definition given in graphql.org

That’s the simplest explanation one can have for GraphQL. We’ll go over it with a simple example anyway.

Let's take an example of a REST endpoint /company that provides complete details of a company like a name, # of employees, address and list of employees details. And we have a client which requires only the name and address of a company.

REST API response for /company endpoint

Certainly, we are providing more data than what is required by the client, isn’t it? In GraphQL the client is expected to request the server with a query which describes just the information it requires from the server. And the server provides the client requested data alone as the response. The example below shows exactly what it does.

Query request from a client for the required information — Just the information needed by the client from the server.

This is great right!!! This is how the issue of Overfetching is being solved by GraphQL. You can find more features provided by graphql here. So by getting to know about the basics of GraphQL we can start discussing our topic now.

SO why not GraphQL?

Even though GraphQL provides lots of feature support which proves it to be flexible and efficient against client requirement, its not that straight forward for any team which is relying completely on REST endpoints. And GraphQL is highly recommended to the product which has multiple clients accessing the same data. It will take lots of development efforts and testing time to put into the production environment with the same resources provided by existing REST APIs. Certainly, it’s not worth the time spending on implementing GraphQL for an application which serves just one client but expects the same data in different naming conventions.

REST API Projection

GraphQL has provided a great concept of accepting a query as a request from the client and working on it to prepare the response from it. Why not take the same concept to provide the only required response via REST API?

In brief, what we are about to do is after the actual API response is created, based on the query sent by the client we will be creating a new JSON object by projecting only the requested information.

Query request expected to be sent from the client by the server. Requesting for name, noOfEmployees and list of employee details. Address is not been requested.

Let’s get to the coding part.

We’ll be needing the GSON package to parse through the response object created by our API and the query request sent by the client parallelly.

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>

I’ve got this QueryResolverService which has the implementation for projecting the response based on the query.

This is the root method which accepts the requested query as a string and response payload as a generic type and produces a JsonElement as a response to the client after resolving the query with the payload.

GSON does an efficient job in converting a string. Once we have the request query and the response payload converted to JsonElement, we can carry out our core logic to create the resolved JsonElement.

It’s a simple recursive logic that does the job pretty well. It’ll be able to handle any type of JSON model object, from a simple JSON holding primitives to a complex one consisting of multiple objects.

Aliases

There is one more interesting feature if you have observed these examples, the reason we are accepting the request query in a JSON format is to provide alias support.

Trimmed request sent by the client to the server with noOfEmployees field renamed to empCount

In the example, noOfEmployees seems like a long name. What if the client want’s to choose a different one in the response which it receives. Just by sending a different name as a value for the same field, like empCount would suffice the requirement. The client has requested only for name and no of employees, and the server has sent just that with field noOfEmployees renamed to empCount.

Consuming the Service

One has to just plug this service to the controller with the response object before returning it to the client without changing any bit of business logic. gson.toJson(resolved) — this is important as without this conversion you might not able to serialize the response. We have to accept a query string as a request payload to project the response. Just this change will make your REST API endpoint enabled with GraphQL like response projection, solving your overfetching issue.

This is the end of the road for you if you’re looking for just the solution. Rest of the blog deals with wiring this service to the application and abstracting the developers from adding more code to their controllers. If you are interested in Aspect Oriented Programming AOP and creating your own annotation , do hang around.

Abstracting the Service

No developer will be interested to use a service which adds more code to existing ones. As a spring-boot developer, we would be very much like to add an annotation to our method and consume the feature effortlessly. Moving forward we’ll be able to make our service abstracted behind an annotation using the help of Spring AOP.

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<scope>compile</scope>
</dependency>

Adding these two packages to our spring-boot application would enable AOP.

What is this AOP again??? — Aspect-Oriented Programming (AOP) complements Object-Oriented Programming (OOP) by providing another way of thinking about program structure. As it would require a blog about itself we’ll skip’em. You can continue reading about AOP here.

We’ll have a ProjectionAspect class via which the service will be abstracted. The core idea is to execute the QueryResolverService after the response entity is been created by the controller.

Annotating a class with @Aspect will make sure it is to be treated as an Aspect in Spring. ProceedingJoinPoint, @Around and @PointCut — these are a few of many AOP concepts that are available. You’ll be able to get to know about them here. We should make sure to execute this service before and after the execution of any controller endpoints. @Around advice gives a perfect sense here to be used, as it enables us to execute the code before the execution of actual code and after the execution is completed as well.

@Pointcut("execution(* com.vinteck.example.apiresponseprojection.controllers.*.*(..))")

Defining the PointCut like this will wire this aspect to every method defined under this package. Which is certainly not expected of us. And we would be requiring the query request from the client as well at this point. How to acquire that now?

@Projected Annotation

We had been speaking about abstracting the service behind an annotation. This is where an annotation comes in handy. Using an annotation will help us able to control wiring the aspect to the endpoints we like to support API Projection.

Setting the @Target to ElementType.METHOD specifies the annotation to be used as a method level one. And there is one more method — queryAtPosition(), this is how we’ll be letting the aspect know how to get the query requested by the client to resolve the projected response.

By giving the @Around advice the value as allControllerEndpoint() && @annotation(projected) ensures that this aspect is available only for controller endpoints which are annotated with @Projected. ProceedingJoinPoint will hold args as well as other meta information that are being passed to the method associated with this aspect. Let's assume that for now the projected.queryAtPosition() contains the position of the query request in the array args provided by joinPoint.

We first let the joinPoint to proceed with the code execution of the controller, we are interested only on the response entity it generates. We work with resolving the query only if we have a 2xxSuccessFullresponse provided by the controller endpoint. Since we have the response entity from the controller and the query associated to resolve the projection, our only task left is to make the QueryResolverService proceed with the resolution. Now we can create a new response entity with the resolved response and return it.

Consuming the service

Just by adding @Projected annotation we are able to consume the QueryResolverService now. Easy-peasy. We have to pass in the position at which the query string is being placed in the method signature. This way we’ll let the aspect know of the position to pick the query from the array of args.

Hurray!!!. Without the support of the GraphQL library, we are able to overcome the issue of Overfetching. Along with that, the alias support is super cool too. And abstracting the service with annotation will let any developer use this at will, with a simple signature change to the controller method. With little more tweaks to the service, we should be able to overcome the data type conversion and parallel processing of array types.

AND that's the end of this blog post. The complete example can be found here.

--

--

Vinodh Subramanian

Software engineer @ Walmart Labs and a Tech enthusiast.