Brightgrove sponsored Compass to Care’s 6th Annual 5K
Our VP of Biz Dev George got your message and will get back to you as soon as possible. Usually, 1-2 days. Have a cup of joe and read more of our stories ;)
Stanislav Silin, Senior .NET Developer
März 11, 2024
Hi, my name is Stanislav Silin, and I’m a Senior .NET Software Engineer at Brightgrove. In this article, I want to showcase a new way to create web APIs with GraphQL.
We’ll look at its benefits compared to the traditional REST API and how to transform existing REST APIs to support GraphQL.
First, we need to figure out what issues we can have with REST APIs and how GraphQL can help solve them. The two main issues would be under-fetching and over-fetching.
They are common when we’re working with REST APIs. Under-fetching occurs when an initial API request doesn’t supply enough data, forcing the application to make additional requests. Over-fetching, on the other hand, happens when an API request returns more data than the client needs.
Consider a scenario where a developer works on a user profile that needs to load personal information (e.g., name, email, profile picture) and user roles (e.g., admin, subscriber, etc.). In a traditional RESTful approach, fetching this data could require at least two different endpoints: one for personal information and another for roles.
If you fetch from the personal information endpoint and it doesn’t include the user roles, you’ve been under-fetched and must make an additional request to the roles’ endpoint.
Conversely, if the personal information endpoint returns all available data about a user—including details not needed for the current view—you’ve over-fetched, wasting bandwidth and potentially slowing down the application.
GraphQL helps to solve this issue rather easily. Instead of having multiple endpoints, there’s a single endpoint that accepts queries. These queries define exactly what data the client needs, eliminating under-fetching. The client can request different data types in a single request.
Let’s look at our user profile example. Instead of making separate requests for personal information and roles, we can make a single GraphQL query:
query {
user(id: 1) {
name
email
profilePicture
roles {
title
}
}
}
This query asks for specific fields (name, email, profilePicture, and roles) for the user with an id of 1. The server will return a JSON object that mirrors the shape of the query and includes just the requested fields.
The flexibility of GraphQL queries also solves the problem of over-fetching. The server will never send unnecessary data as the client specifies exactly what data it needs. Using our example, if we don’t need the user’s profile picture for a certain view, we can simply leave it out of the query:
query { user(id: 1) { name email roles { title } } }
The server will not send the profile picture in this query, even if available.
Query
GraphQL’s strict protocol is an essential part of its power and is a key distinction from REST APIs, where conventions can vary significantly. Nowadays, all major tech companies have their own REST best practices that sometimes can be totally incompatible.
At the same time, GraphQL APIs are organized in terms of types and fields, not endpoints. GraphQL uses a strong type system to describe the capabilities of an API and all the types of data it can return, enabling developer tools and ensuring the APIs behave in predictable ways.
Here is an example of how the schema may look:
type Query { users(page: Int!, size: Int!): [User!]! user(id: Int!): User } type Mutation { createUser(name: String!, email: String!): User! } type Role { id: Int! name: String! } type User { id: Int! name: String! email: String! roles: [Role!]! }
Versioning
Versioning is essential to maintaining APIs, especially when the API undergoes significant changes. In traditional REST APIs, versioning often involves either changing the URL of an endpoint or including a version number in the request header.
This approach has the downside of potentially breaking the application for any clients not updated to use the new API version. However, GraphQL takes a fundamentally different approach to versioning, helping avoid the issues associated with traditional versioning methods.
Instead of creating new versions of the entire API, developers can simply extend the existing GraphQL schema with new fields to support new functionality. Clients that haven’t been updated to understand these new fields will just ignore them, and the API will remain backward compatible.
If we need to remove a field, GraphQL supports deprecation, which signals to developers that a field is no longer in use and will be removed in the future. Even after a field is deprecated, it still works, so clients using that field will not break immediately. This provides time for clients to transition away from the deprecated field.
The strong type system in GraphQL helps to ensure that changes are compatible with existing queries. It prevents incompatible changes like altering the type of a field.
In essence, GraphQL’s approach to versioning avoids the need for version numbers and breaking changes. It’s more flexible and forgiving and lets developers add new features to an API without disrupting existing users. As a result, API evolution becomes a smoother process, fostering a better developer experience and ensuring more reliable applications.
Client Generation
In GraphQL, client generation leverages the API schema and introspection feature. Given that the GraphQL schema is strongly typed and the types and relationships are well-defined, it’s possible to use this information to generate client code automatically.
The client can be in various languages like TypeScript, C#, etc. It reduces the amount of boilerplate code that developers need to write, saving time and minimizing the risk of manual coding errors.
Client generation works harmoniously with GraphQL’s field deprecation semantics. When a field is deprecated, it can still be included in the schema and the generated client code, but it’s marked as deprecated.
Developers can see the deprecation warning directly in their IDE while coding. This helps them move away from using deprecated fields in a smooth and controlled manner. Over time, as developers stop using the deprecated field, it can be safely removed from the schema without breaking any clients.
Initial setup
Let’s look at converting an existing REST API into a GraphQL API. The initial endpoints look like that:
app.UseEndpoints( o => { o.MapSwagger(); o.MapGet("/users", async (int page, int size, UserService service) => await service.GetUsers(page, size)); o.MapGet("/users/{id}", async (int id, UserService service) => await service.GetUser(id)); o.MapGet("/users/{id}/roles", async (int id, RoleService service) => await service.GetRolesByUserId(new[] { id })); o.MapPost("/users", async (UserCreationRequest request, UserService service) => await service.CreateUser(request.Name, request.Name)); });
Here’s an example of endpoints that handle the page from under-fetching and over-fetching. The first thing you need to do is to install the HotChocolate.AspNetCore NuGet package.
dotnet add package HotChocolate.AspNetCore
HotChocolate is a set of tools that allows us to receive and process GraphQL queries. It has a nice integration with the AspNet Core pipeline. The full documentation can be found here.
Now to the bootstrap GraphQL server:
+ services + .AddGraphQLServer() + .AddQueryType<Query>(); // ... app.UseEndpoints( o => { + o.MapGraphQL(); o.MapSwagger(); o.MapGet("/users", async (int page, int size, UserService service) => await service.GetUsers(page, size)); // ... + public class Query + { + public DateTimeOffset Utc() => DateTimeOffset.UtcNow; + }
When we launch our application, we can access the GraphQL endpoint http://localhost:1234/graphql. If opened via the browser, we will see a GraphQL IDE. Here we can check the GraphQL schema and execute requests.
The initial setup is ready, and we can start to transform our REST APIs.
Creating queries
Let’s transform the next endpoint first:
o.MapGet("/users/{id}",
async (int id, UserService service) => await service.GetUser(id));
To do it, we can just create a new method in our Query class:
public class Query { public DateTimeOffset Utc() => DateTimeOffset.UtcNow; + public Task<User?> GetUser(int id, [Service] UserService userService) + => userService.GetUser(id); }
Done! If we open our GraphQL IDE and check the GraphQL schema, it will look like that:
type Query { utc: DateTime! user(id: Int!): User } type User { id: Int! name: String! email: String! }
The HotChocolate automatically detects what types are exposed and adds them to the schema.
Let’s move on and transform another endpoint. However, now we’ll do it a bit differently. We can still add it to the Query class, but placing everything into one class is not a great idea. We can split it to look very close to what we have when using controllers from AspNet Core.
To make it work, create a new class UserQueryExtensions and define new “endpoints” there. Moving further, I will use the “GraphQL field” definition to indicate the equivalent to “REST endpoints”, but for GraphQL.
It will look like this:
services .AddGraphQLServer() .AddQueryType<Query>() + .AddTypeExtension<UserQueryExtensions>(); // .. public class Query { public DateTimeOffset Utc() => DateTimeOffset.UtcNow; - public Task<User?> GetUser(int id, [Service] UserService userService) - => userService.GetUser(id); } + [ExtendObjectType(typeof(Query))] + public class UserQueryExtensions + { + public Task<IEnumerable<User>> GetUsers(int page, int size, [Service] UserService userService) + => userService.GetUsers(page, size); + + public Task<User?> GetUser(int id, [Service] UserService userService) + => userService.GetUser(id); + }
Here we created a class that extends another GraphQL type. In our case, it is a root-type Query. Also, I moved the user-related GraphQL field from the Query and added it to a new extension.
Pay attention to the configuration section at the beginning. You have to add these extensions to the setup manually. Otherwise, HotChocolate won’t detect them.
Now the GraphQL schema will have the following look:
type Query { utc: DateTime! + users(page: Int!, size: Int!): [User!]! user(id: Int!): User } type User { id: Int! name: String! email: String! }
As you can see, from the GraphQL perspective, only one thing changed. Now, we have a new GraphQL field. It is exactly what we wanted.
That’s it for today. Join me in the second part of the article to learn more about complex type extending and creating mutations. Thanks for your attention, and until next time!
Discover more about our experiences, victories, struggles, and best practices