REST vs. GraphQL: differences, benefits, and conversion. Part 2


März 11, 2024

Let’s get back to business. Today, we’ll look at some particularities of switching to GraphQL.

Let’s add roles to our GraphQL schema. The initial REST endpoint looks like this:   

 o.MapGet("/users/{id}/roles",
async (int id, RoleService service) => await service.GetRolesByUserId(new[] { id }));   

We can do this in two ways. The first one is a more REST-like way. We just have to create another GraphQL field that will return a list of roles for a specific user. The schema would look like this:   

type Query {
utc: DateTime!
users(page: Int!, size: Int!): [User!]!
user(id: Int!): User
+ userRoles(userId: Int!): [Role!]!
}

type User {
id: Int!
name: String!
email: String!
}

+ type Role {
+ id: Int!
+ name: String!
+ }

But there is also a GraphQL way. We can extend the User type itself. So, when we request a user, we can fetch additional user roles. In this case, we will have the next GraphQL schema:   

type Query {
utc: DateTime!
users(page: Int!, size: Int!): [User!]!
user(id: Int!): User
}

type User {
id: Int!
name: String!
email: String!
+ roles: [Role!]!
}

+ type Role {
+ id: Int!
+ name: String!
+ }

Let’s do it the second way. To make it work, we need to create another extension class. However, we will extend the User type, not Query:   

[ExtendObjectType(typeof(User))]
public class UserRolesQueryExtensions
{
public async Task<Role[]> GetRoles([Parent] User user, [Service] RoleService roleService)
{
var rolesByUserId = await roleService.GetRolesByUserId(ids);
return rolesByUserId[user.Id];
}
}

This setup allows us to build the next Query from the “under- and over-fetching” example:   

query GetUserProfile($userId: Int!) {
user(id: $userId) {
id
name
email
roles {
id
name
}
}
}

But how does it work? The GraphQL server receives the request mapped to proper GraphQL extensions. Here it would be two of them—GetUser and GetRoles. When the first extension is executed, its result is passed to the second one.

 

In the end, we have one request from the client and the GraphQL server that executed two GraphQL fields. As a result, the client will receive the final result faster because everything happens locally on the server, which is much closer to the database.   

   

However, there is one issue with such an approach. Let’s have a look at the next query:

query GetUsers($page: Int!, $size: Int!) {
users(page: $page, size: $size) {
id
email
roles {
id
}
}
}

Here we have a GraphQL field that returns a list of items. Then, for each item, we have an extension. If we keep everything as we have right now, it can be a disaster.

   

Let’s analyze what is happening:   

  1. The GraphQL server executes the GetUsers GraphQL field and gets 5 items.   
  2. The GraphQL server executes the GetUserRoles GraphQL field for the 1st item.   
  3. The GraphQL server executes the GetUserRoles GraphQL field for the 2nd item.   
  4. The GraphQL server executes the GetUserRoles GraphQL field for the 3rd item.   
  5. The GraphQL server executes the GetUserRoles GraphQL field for the 4th item.   
  6. The GraphQL server executes the GetUserRoles GraphQL field for the 5th item.   
  7. The GraphQL server returns the final result.   

As you can see, it can be an issue when GetUserRoles goes to the database for every item. It can make the performance really bad. The GraphQL server has a feature called data loaders to fix this issue. It allows you to batch GraphQL field execution and get data in one go. Here is what the improved GetUserRoles would look like:   

[ExtendObjectType(typeof(User))]
public class UserRolesQueryExtensions
{
public async Task<Role[]> GetRoles(
IResolverContext context,
[Parent] User user, [Service] RoleService roleService)
{
return await context.BatchDataLoader<int, Role[]>(async (ids, ct)
=> await roleService.GetRolesByUserId(ids))
.LoadAsync(user.Id);
}
}

It creates an instance of batch data loader on the first call and adds the current id to a list. Then, for every subsequent call, we just remember user ids. When we have the full list of user ids, it calls a lambda with those user ids, and we can just get user roles for all users at once. In this case, the log would look like this:   

  1. The GraphQL server executes the GetUsers GraphQL field and gets 5 items.   
  2. The GraphQL server executes the GetUserRoles GraphQL field that creates batch data loader and passes the id of the 1st item.   
  3. The GraphQL server executes the GetUserRoles GraphQL field that passes the id of the 2nd item.   
  4. The GraphQL server executes the GetUserRoles GraphQL field that passes the id of the 3rd item.   
  5. The GraphQL server executes the GetUserRoles GraphQL field that passes the id of the 4th item.   
  6. The GraphQL server executes the GetUserRoles GraphQL field that passes the id of the 5th item.   
  7. The GraphQL server executes the data loader.   
  8. The GraphQL server returns the final result.   

Full documentation can be found here.   

Creating a mutation

Another small thing about GraphQL. You may have forgotten, but there was another endpoint:   

 o.MapPost("/users", async (UserCreationRequest request, UserService service) 
=> await service.CreateUser(request.Name, request.Name));

Semantically, it has a different meaning compared to previous endpoints. It is a mutation operation that creates a new entity. GraphQL draws a line between read and edit operations. All read operations have to be defined in Query type, and all edit operations have to be defined in Mutation type. So, defining this endpoint as a mutation would be a good idea.   

 

Here is how we can do it:   

services
.AddGraphQLServer()
.AddQueryType<Query>()
+ .AddMutationType<Mutation>()
+ .AddTypeExtension<UserMutationExtensions>();

// ...

public class Mutation
{

}

// ...

[ExtendObjectType(typeof(Mutation))]
public class UserMutationExtensions
{
public Task<User> CreateUser(string name, string email, [Service] UserService userService)
=> userService.CreateUser(name, email);
}

It’s the same approach as with the Query type, but now we have the Mutation type as the root type.   

Conclusion

First, we had a brief look at the reasons why GraphQL was developed. Under-fetching and over-fetching are common challenges with traditional RESTful APIs, potentially leading to inefficiencies and poor user experiences.   

 

GraphQL offers a way to avoid it, allowing clients to specify precisely what data they need and reducing unnecessary requests and data transfer. This flexibility optimizes performance and bandwidth usage, making GraphQL a compelling choice for modern web development.   

 

Then, we looked at how to set up a GraphQL server. We figured out how to create GraphQL fields and what we need to consider when transforming existing REST endpoints into GraphQL fields.   

 

I hope you find these articles useful. Next time, we’ll look at how to do integration testing of our GraphQL APIs.