GraphQL is a query language for APIs and a runtime for fulfilling those queries. It is widely used for various client projects here at Accedia including web applications for the housing market, for various media and press corporations, and more.
Taking into account the popularity of the language, in this blog post, we will discuss how to organize and structure a GraphQL for .NET projects. This is a topic of great importance when we expect to either build a rather large project or we have to move our existing solution to GraphQL. Therefore, if we can set certain rules and guidelines on how to organize and write the code in advance, we will be able to tackle the high complexity of such projects. In the code examples below, I will be using GraphQL Conventions NET, which utilizes the GraphQL NET library, but the concepts which we will be discussing can be applied anywhere.
what can we find within a single graphql project?
Here is the minimum set of building parts required for our current scenario:
- Schema ([ApplicationName]Schema.cs): It serves as an entry point for the application and defines all supported operations. The minimum requirement to have a working schema is to have a definition for a Query.
- Query ([ApplicationName]Queries.cs): If we can compare Queries to the REST service, Queries contain all GET endpoints serving data to the users.
- Mutation ([ApplicationName]Mutatations.cs): These are all the possible ways to change any of our entities, regardless of whether we are updating, deleting, or creating them.
- Subscription ([ApplicationName]Subscriptions.cs): If we want to allow the Graph users to maintain an active connection to our server (most commonly via WebSocket) and track how the entities are changing, this is the way to achieve that.
- Query Types ([ApplicationName]QueryTypes.cs): Each application exposes Models to its users. With GraphQL they are regarded as Query types.
- Input Types ([ApplicationName]InputTypes.cs): Sometimes we need to provide input to the GraphQL server. In order to differentiate it from the output, it can be stored in a separate file.
- Subscription Types ([ApplicationName]SubscriptionTypes.cs): If we want a clear separation between the result of a query operation and a subscription operation, their output can also live in separate files.
WHAT is the standard approach to organizing a graphql project?
Learning something new is often challenging. Therefore, when we are handled with the task to build a GraphQL solution for our company (either a new or existing product), based on our preferred .NET Library for GraphQL we can find good examples, which provide us with outlines and guidance.
two solutions for organizing your graphql project
First option to organize our GraphQL project:
- Project root
- [ApplicationName]Schema.cs file with the schema definition
- [ApplicationName]Queries.cs file containing all operations for getting data
- [ApplicationName]Mutatations.cs describing all options for changing our entities
- Types folder containing the definitions of our types
Can this be a good structure for your project? I would say for POC or demo purposes it can do a great job, as we can quickly access the main operations provided by GraphQL. Also, in the single folder called ‘Types’, we can find all required inputs and Query types.
Second option to organize our GraphQL project:
- Project root this time is simpler, containing only the bare minimum to start the application.
- Each type has its own folder, for example Reviews, Tasks and so on. If we take a look into each of the folders, we will see definitions of:
How do we rate this setup? A pretty good one. When we open the project, we won’t be confused by a large number of files or trying to figure out what is where. Instead, we will have a clear entry point for the application on the root level and then a clear visualization of the types with which the project is dealing. If those types don’t have too many operations related to each of them, the structure can hold small to medium-sized projects.
the drawbacks of the given solutions
Let’s discuss a few of the drawbacks of those setups. In general, they would lead to a big waste of time for the developers, as navigating inside the project would be difficult. Also, the bar for newcomers to the project would be rather high, as they will be facing a lot of questions regarding how to proceed with certain tasks or where to find what.
Some additional drawbacks of the above-mentioned solutions include:
- They are not suitable for big development teams, as we can get merge conflicts when several people end up working on the same file;
- Some classes will grow rather big as a result of placing all Mutations in a single file. And we all know how difficult it can be to navigate in a file that is 10 000 lines long;
- We can end up having way too many files in a single folder. For example, if there are a lot of different operations, the number of files can reach 30-40. This may cause confusion if our screen is not big enough to fit them all;
- When looking at the project, due to either the big number of files or logins, it would be difficult for us to know what the application is about.
how to successfully organize the code of our graphql project?
Now comes the question of what to do when we have a larger project with tens or hundreds of types and operations. How to structure it, so that it can be readable, easy to navigate, and welcoming to new developers? To tackle complexity, we need to create a set of rules and enforce them on any given project. Here are the ones that I think can make our lives easier.
add schema to the root level of the project
First, we need to have a Schema. It’s our main entry to the Graph so it should be visible from a first glance. Inside the Schema, we define all operations that the Graph can perform. In the world of .NET and C#, this would mean generating one big file, unless we use partial classes. If you want to find more information about them you can check out the documentation provided by Microsoft. When we use partial classes, we can define a single class for [ApplicationName]Queries.cs, and then each entity can add an extra query operation to it. The only downside of this approach would be that the partial classes will need to live in the same namespace, which would require some extra awareness from us as developers. A way to avoid that is by adding a custom C# Analyzer rule to hawk over this issue.
Example of [ApplicationName]Schema.cs class implementation:
ADDress shared concerns
When building any solution, at some point we can identify certain routines that are repeating almost everywhere. If we can abstract them in a way that can be used from all entities, then we are ready to create our first Common or Shared folder.
A good example would be the ID type, which is something specific to GraphQL. The most common approach in a traditional application is to use Guid or Int for the ID of the Entity. Given the fact that the Graph layer will need to interact with a database or some sort of a service layer, we will need to have an Extension method that can help us transition between the specific GraphQL ID type and the Guid/Int used in the existing codebase. The result will be an IdExtensions.cs class.
Another specific characteristic of GraphQL is the concept of NonNull. It helps reduce the null checks on the frontend side of the application, while from a backend perspective, we are ensuring to always return a result for certain operations. Here, once again, we can have the implementation in a shared extension file.
separate the code by domain
Most applications are built around different domains. We can create a separate folder for each one of them in order to give a good visual representation. By having the code in a single folder, we will be able to save time when implementing a new feature. Developers, on the other hand, will be able to quickly identify where to add the new files needed for the implementation. This also creates an easy-to-follow directory structure for adding new domains.
Add Queries Folder
Here we need to add all possible query operations related to the current domain. For each operation, we add a single file called [OperationName]Query.cs which should include the definition of any inputs that would be required for the given query. This makes certain parts of the application much easier to review and maintain. By looking at the file names we can learn what the supported operations are.
Here is an example of the implementation of a [OperationName]Query.cs which in this case would be ModelAsQuery.cs:
ADD Mutations folder
What happens if we want to change the state of an object? Once again, all possible operations get their own file named after [MutationName]Matation.cs, clearly describing what kind of input they need in order to perform that operation.
Here is an example of the implementation of a [OperationName]Mutation.cs which in this case would be ModelAUpdateMutation.cs:
ADD subscriptions folder
You won’t believe it, but we organize the subscriptions folder the exact same way as the Mutations and the Queries. Sometimes consistency can be difficult, but over time the benefits behind it can be great.
Let’s take a look at a given [SubscriptionName]Subscription.cs file:
ADD the query types to the domain folder root level
At the end of the day, it’s important what our applications produce as an output. In our GraphQL project that would be the QueryTypes. All domain folders will have at least one of them. By placing them at the root level, we can have very quick access to them.
Let’s take a look at a specific [DomainName].cs file:
What did we achieve with this project organization? First of all, we organized the code into smaller and relatively independent domain parts. Inside them, we managed to clearly label what’s the behavior of each domain and figure out what’s implemented or not with just a glance. Having a good structure also helps new developers to be able to have an impact on the project relatively quickly. Another great benefit is that such a structure also allows us to have a rather large team to work on the GraphQL project without constantly creating conflicts.
Do you need advice on organizing the logic of your GraphQL project? Let me know and I’ll be more than happy to answer any of your questions!