Supercharge Your API Development with GraphQL and Go
Tags: go, graphql, tutorial
Published: 2023-11-02 00:03:33

---
title: Supercharge Your API Development with GraphQL and Go
description: Create a Go-Based GraphQL server with gqlgen, dataloaden, and custom validators
tags:
  - go
  - graphql
  - tutorial
cover_image: ''
series: Developing a GraphQL API with Go
canonical_url: null
published: true
id: 1479976
---

Welcome to the next chapter of our GraphQL journey, where we'll delve into the intricacies of building a high-performance GraphQL API in a Go project. In our previous post, we explored key foundational elements, from creating an application configuration with envconfig to integrating a data persistence layer using Gorm and efficiently indexing data with OpenSearch. Today, we're taking this journey to the next level. We'll introduce you to the powerful gqlgen library and showcase how it effortlessly integrates GraphQL into your Go application. Building upon the project conventions we've already established, we'll define our GraphQL schema, implement resolvers, and explore the abstract world of data models, breaking them down into reusable services. But that's not all – we'll also discuss how middleware can seamlessly integrate these services, and we'll introduce schema-level validation to ensure data integrity. Furthermore, for those seeking enhanced performance and efficient data retrieval, we'll shine a spotlight on dataloaders. So fasten your seatbelts as we embark on a voyage through the fascinating realm of GraphQL and Go.

## Setting up gqlgen for GraphQL Integration

To begin integrating GraphQL into your project, you will need to set up [gqlgen](https://gqlgen.com). gqlgen is a popular Go library for automatically generating a GraphQL server based on a schema definition. It provides a seamless way to build robust and type-safe GraphQL APIs. Here's how you can install and initialize gqlgen:

Start by adding the following line to your `tools.go` file:

```go
import _ "github.com/99designs/gqlgen"
```

Next, run the initialization script for gqlgen using the following commands:

```bash
go mod tidy
go run github.com/99designs/gqlgen init
```

Running `gqlgen init` will generate the `gqlgen.yml` file, which serves as the base configuration for gqlgen. It will also create a `graph` folder containing the necessary files and folders for GraphQL. Some important artifacts that are generated include `schema.graphqls`, which contains the GraphQL schema definition for your API, and the `generated` and `model` folders, which provide generated structs and functions to support your GraphQL implementation.

With the gqlgen skeleton established, let's customize it to fit the Go project convention we've set up in the previous entry of this series.

## Customizing gqlgen for the Go Project Convention

Begin by moving the `graph` folder into the `internal` folder. This ensures that the generated code follows the project's internal structure.

Next, we need to update the gqlgen configuration by modifying the `gqlgen.yml` file. We want gqlgen to recognize the updated `graph` folder and generate the code accordingly. Here's an example of the updated configuration:

```yml
# Where are all the schema files located?
schema:
  - internal/graph/schemas/*.graphqls

# Where should the generated server code go?
exec:
  filename: internal/graph/generated/generated.go
  package: generated

# Where should the generated models go?
model:
  filename: internal/graph/model/models_gen.go
  package: model

# Where should the resolver implementations go?
resolver:
  layout: follow-schema
  dir: internal/graph
  package: graph

# Other gqlgen configuration values
```

The modified configuration allows gqlgen to recognize multiple `.graphqls` files located in the `internal/graph/schemas` directory, enabling better compartmentalization of the schema.

## Defining the GraphQL Schema

In GraphQL, we need to define several primary elements: model types, connection types, input types for CRUD operations, and resolvers. Let's focus on an example for the `Ingredient` type:

```graphql
type Ingredient {
  id: ID!
  name: String!
  measurement: Measurement!
  createdAt: Time!
  updatedAt: Time!
}

type IngredientList {
  ingredients: [Ingredient!]
  lastItem: String!
  hits: Int!
}

input NewIngredient {
  name: String!
  measurement: String!
}

input UpdateIngredient {
  name: String
  measurement: String
}

type IngredientOps {
  update(id: String!, input: UpdateIngredient!): Ingredient! @goField(forceResolver: true)
  delete(id: String!): Ingredient! @goField(forceResolver: true)
}
```

The above snippet represents the `Ingredient` type in our schema. The schema file for `Ingredient` would be located at `internal/graph/schemas/ingredient.graphqls`. It includes definitions for the `Ingredient` type itself, an `IngredientList` type used for retrieving multiple records, input types for insertions and updates, and a type to compartmentalize mutations related to `Ingredient`.

By defining these elements in the schema, we establish the structure and behavior of our GraphQL API for the `Ingredient` type.

Once all the data model types are defined, it's time to integrate them with the root schema, which is located in the `schema.graphqls` file.

```graphql
directive @goField(
  forceResolver: Boolean
  name: String
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION

scalar Time

type Query {
  ingredient(id: String!): Ingredient!
  ingredients(name: String): IngredientList!
}

type Mutation {
  ingredient: IngredientOps! @goField(forceResolver: true)
}
```

In the schema, we define a custom scalar `Time`, which represents a `time.Time` object in Go within the context of our GraphQL schema. This scalar is built-in and supported by gqlgen.

GraphQL specifies three root types: `Query`, `Mutation`, and `Subscription`. In our example, we focus on `Query` and `Mutation`. The `Query` type is responsible for data retrieval operations, while the `Mutation` type handles updates to the data in the schema. We won't be using the `Subscription` type for our API server in this example.

gqlgen provides a built-in directive called `@goField`, which allows us to override the default behavior in GraphQL. We can use `@goField` to force specific fields to be treated as resolvers, enabling us to compartmentalize our mutations and customize their behavior as needed.

We can update the generated code by executing the following command:

```bash
go run github.com/99designs/gqlgen generate
```

This command triggers gqlgen to regenerate the necessary code based on the schema and configuration files. It ensures that any changes made to the schema or configuration are reflected in the generated code, providing an up-to-date implementation for our GraphQL API.

## Implementing Resolvers

After executing the `gqlgen generate` command, you will notice several new files in the `internal/graph` folder. Each model defined in the previous section will have a corresponding `.resolvers.go` file, which contains the implementation of the schema resolvers. Initially, these files will contain stubbed-out resolver methods.

For example, let's consider the integration of mutations with the `Ingredient` type:

```go
package graph

func (r *ingredientOpsResolver) Update(ctx context.Context, obj *model.IngredientOps, id string, input map[string]interface{}) (*model.Ingredient, error) {
    // Resolver implementation here
}

type IngredientOpsResolver struct {
    *Resolver
}
```

In the above snippet, we have the resolver implementation for the `Update` mutation of the `Ingredient` type. Additionally, a `schema.resolvers.go` file is generated, which includes resolver functions for both queries and mutations:

```go
package graph

func (r *queryResolver) Ingredient(ctx context.Context, id string) (model.Ingredient, error) {
    // Resolver implementation here
}

func (r *mutationResolver) Ingredient(ctx context.Context) (*model.IngredientOps, error) {
    return &model.IngredientOps{}, nil
}

func (r *Resolver) Mutation() generated.MutationResolver {
    return &mutationResolver{r}
}

func (r *Resolver) Query() generated.QueryResolver {
    return &queryResolver{r}
}

type mutationResolver struct {
    *Resolver
}

type queryResolver struct {
    *Resolver
}
```

In the `schema.resolvers.go` file, we have resolver methods for the `Ingredient` query and the `Ingredient` mutation. These methods will be implemented with the required functionality to handle the corresponding GraphQL operations.

Overall, after generating the code with `gqlgen`, the resolver files provide a starting point for implementing the actual logic for the resolvers, allowing you to customize the behavior of your GraphQL API.

## Abstracting the Data Model: Implementing Services

While we could proceed to call our data model methods directly from the resolvers we just generated, as a general best practice, we'll wrap our data model with a layer of abstraction called services. This approach provides several benefits and allows us to maintain separation of concerns. In the `internal/graph/services` folder, we'll create a service for each data model defined in the previous article.

Each service will define an interface, `IIngredientService` in the case of the `Ingredient` model, which outlines the operations that can be performed on that specific data model. This interface serves as a contract and provides a generic abstraction for our resolvers. By working with interfaces, we can easily change the implementation of each service without impacting the resolvers. This flexibility comes in handy, especially when writing unit tests, where we can swap out a service implementation for a mock implementation.

In the example of the `IngredientService`, we define two package aliases: `dbmodel` refers to the database model package created with gorm, and `apimodel` refers to the generated GraphQL model created with gqlgen. The `IngredientService` struct holds the necessary dependencies such as the database connection (`DB`), the OpenSearch connection (`ES`), and the context (`Context`).

Within the `IngredientService`, we implement the methods defined in the `IIngredientService` interface. For instance, the `Create` method takes input from the GraphQL API, creates a new `dbmodel.Ingredient` instance, and saves it to the database using `svc.DB.Create()`. We can also perform additional logic or transformations as needed before returning the result converted to the GraphQL model (`apimodel.Ingredient`).

```go
package services

type IIngredientService interface {
    Create(input apimodel.NewIngredient) (*apimodel.Ingredient, error)
    Update(id string, input map[string]interface{}) (*apimodel.Ingredient, error)
    Delete(id string) (*apimodel.Ingredient, error)
    FetchIngredient(id string) (*apimodel.Ingredient, error)
    FetchIngredients(id string) (*apimodel.IngredientList, error)
}

type IngredientService struct {
    DB      *gorm.DB
    ES      index.OpenSearchConnection
    Context context.Context
}

func (svc *IngredientService) Create(input apiModel.NewIngredient) (*apimodel.Ingredient, error) {
    ingredient := dbmodel.Ingredient{
        Name:        input.Name,
        Measurement: input.Measurement,
    }

    if err := svc.DB.Create(&ingredient); err != nil {
        log.Errorf("[IngredientService] could not save Ingredient => %v", err)
        return nil, err
    }

    return ingredient.ConverToGraphQLModel(), nil
}

// Implement remaining service methods

type Services struct {
    UserService IUserService
}
```

The `Services` struct acts as a container for all the defined services, providing a convenient way to manage and access them in our application.

By introducing this services layer, we achieve better organization, maintainability, and testability of our codebase.

### Integrating Services Using Middleware

With our services implemented, the next step is to integrate them into our GraphQL implementation through the use of middleware. We'll leverage the HTTP server framework, gin-gonic, to set up the middleware. To begin, let's create a new folder called `internal/middleware`, which will contain our middleware files. Within this folder, we'll create a file named `services.middleware.go`. In this file, we'll define a function called `Services` that will serve as the middleware function, enhancing the server context with the services we defined earlier. Additionally, we'll create a function named `ForServices` that will be used to fetch the services from the server context.

```go
package middleware

var serviceKey = &contextKey{name: "services"}

// Services enhances the request context with the services
func Services(db *gorm.DB, es index.OpensearchConnection) gin.HandlerFunc {
    return func(c *gin.Context) {
        services := services.Services{
            UserService:       &services.UserService{DB: db, ES: es, Context: c.Request.Context()},
            PantryService:     &services.PantryService{DB: db, ES: es, Context: c.Request.Context()},
            IngredientService: &services.IngredientService{DB: db, ES: es, Context: c.Request.Context()},
        }
        // Enhance the context with the services
        c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), serviceKey, &services))
        c.Next()
    }
}

// ForServices is used to retrieve the service directory from the context
func ForServices(ctx context.Context) *services.Services {
    return ctx.Value(serviceKey).(*services.Services)
}
```

The `Services` function is a gin.HandlerFunc that takes a database connection (`db`) and an OpenSearch connection (`es`) as parameters. Within this function, we create instances of the services we defined earlier, passing the necessary dependencies. The middleware then enhances the request context by adding the services to it using `context.WithValue()`. The `ForServices` function retrieves the services from the context based on the `serviceKey`.

Within the context of our resolvers, we can now leverage our service implementation. For example, let's consider the implementation of an ingredient resolver:

```go
func (r *ingredientOpsResolver) CreateIngredient(ctx context.Context, obj *apimodel.IngredientOps, input apimodel.NewIngredient) (*apimodel.Ingredient, error) {
    services := getServices(ctx)
    return services.IngredientService.Create(input)
}
```

In this code snippet, we define the resolver method `CreateIngredient` which takes the necessary context, the object being resolved (`obj`), and the input data (`input`). By calling `getServices(ctx)`, we retrieve the services from the context. With the services available, we can then invoke the `Create` method of the `IngredientService` to add a new ingredient based on the provided input. This integration of services within our resolvers allows us to easily interact with the underlying data models and perform the necessary operations.

## Schema-Level Validation

To ensure the stability and security of your API, it is crucial not to blindly trust user input. One of the primary steps in handling incoming requests is validating the request body. By validating the request body, you can ensure that only expected and valid requests are processed, while also safeguarding your API against potential malicious code or attacks.

Validating the request body serves as a defense mechanism to filter out any inappropriate or malformed input that could potentially compromise the integrity and security of your system. It allows you to verify the data's format, check for required fields, enforce data constraints, and perform any necessary sanitization or normalization.

By implementing robust input validation mechanisms, you establish a strong line of defense against common security vulnerabilities such as injection attacks, cross-site scripting (XSS), and other forms of malicious exploits. It helps to mitigate the risk of unauthorized access, data breaches, and manipulation of sensitive information.

In addition to ensuring security, proper input validation also contributes to the overall stability and reliability of your API. By rejecting invalid or unexpected requests early in the process, you can prevent errors, inconsistencies, and potential crashes that could occur when handling malformed data. This improves the overall robustness and performance of your API.

Transitioning to the built-in validations provided by GraphQL, you can leverage the native validation capabilities of the GraphQL specification itself. GraphQL offers a range of predefined validation rules that can be applied to your schema and queries, ensuring data integrity and consistency.

By utilizing these built-in validations, you can further enhance the validation process within your API. The GraphQL validation rules not only validate the incoming request body but also leverage the GraphQL schema to ensure the validity of the entire operation.

The GraphQL schema acts as a blueprint for your API, defining the types, fields, and relationships that your API supports. During the validation process, GraphQL's built-in validations compare the structure and semantics of the incoming request against the schema. This schema-based validation ensures that the requested fields, arguments, and operations conform to the defined types and their respective constraints.

By incorporating the schema in the validation process, you benefit from a powerful mechanism that checks not only the basic syntax but also the semantic correctness of the request. The validation rules analyze the query's structure, field usage, argument requirements, input object validation, and more, ensuring that the request aligns with the schema's expectations.

The benefit of using GraphQL's built-in validations, which are schema-aware, is that they provide a standardized and declarative approach to ensure data quality. The validations are performed before executing the resolver functions, guaranteeing that the incoming request aligns with the schema's definitions.

This schema-based validation approach offers several advantages. First, it allows you to catch and handle potential issues early on, reducing the chances of executing invalid or inconsistent operations. Second, it enables you to provide meaningful and precise error messages to clients, guiding them in making valid requests by highlighting specific violations against the schema. Third, it helps maintain the consistency and coherence of your API's responses by ensuring that the data returned adheres to the defined schema and meets the expected structure.

Moreover, GraphQL's validation rules are extensible, allowing you to add custom validation logic tailored to your specific business requirements. This flexibility enables you to enforce additional business rules, perform complex validations, and integrate with any existing validation frameworks or libraries, all while leveraging the schema as the foundation for validation.

We will extend GraphQL's built-in validation logic by introducing a custom directive that enables field-level validation, allowing us to impose additional constraints on specific fields. To begin, we'll define the new directive in our `schemas.graphqls` file, as shown in the snippet below:

```graphql
directive @validate(constraint: String!) on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
```

This directive, named `validate`, will be utilized on query arguments and fields within our `input` types. By applying this directive, we can specify a constraint that must be satisfied by the input provided for the annotated field.

With the `validate` directive established in the schema, the next step is to provide the business logic for the directive. Gqlgen requires directive logic to be located in the `graph/directives` directory. We'll create a file named `validate.go` which will contain the implementation of the `validate` directive.

The validator implementation will make use of the [go-playground/validator](https://github.com/go-playground/validator) package. This package provides several common validators that can be applied using Go struct-tags. To get started, we will create a package-level initialization function `init()` and a public function `Validate()` as shown below:

```go
package directives

import (
	"context"
	"github.com/go-playground/validator/v10"
	"github.com/graph-gophers/graphql-go"
)

var (
	validate *validator.Validate
)

func init() {
	validate = validator.New()
}

func Validate(ctx context.Context, obj interface{}, next graphql.Resolver, constraint string) (interface{}, error) {
	val, err := next(ctx)
	if err != nil {
		panic(err)
	}

	err = validate.Var(val, constraint)
	if err != nil {
		return nil, err
	}

	return val, nil
}
```

The function `Validate()` effectively applies the specified constraint to validate the `val` using the `validate` package. The `init()` function initializes the `validate` package's validator, which is crucial for this validation process.

The introduction of validator directives enriches our GraphQL schema by allowing us to enforce field-level constraints. By leveraging the go-playground/validator package, we can efficiently validate input data with predefined constraints. These directives add a layer of security and data integrity, ensuring that only valid data is accepted. What's particularly advantageous is that directives offer a modular and compartmentalized structure, making it easy to apply and reuse validation rules across different parts of the schema. This modular approach promotes maintainability and consistency, as validation logic is separated from resolver functions. It's a powerful way to ensure that your GraphQL API adheres to business rules and data integrity requirements without cluttering your resolver code.

## Faster Data Retrievers with dataloaden

Having covered how to create custom directives like `validate` to enforce validation rules within our GraphQL schema, let's now delve into another important aspect of GraphQL server optimization and data handling: data loaders. While validators ensure the integrity of the input data, data loaders are crucial for efficiently fetching and aggregating data from various sources in response to GraphQL queries. GraphQL provides a powerful mechanism for efficient data loading known as _dataloaders_. These data loaders help mitigate the N+1 query problem by batching and caching database queries, significantly enhancing the performance and efficiency of your GraphQL APIs. In the next section, we'll explore how to implement data loaders in our GraphQL server and optimize data retrieval.

This is where [dataloaden](https://github.com/vektah/dataloaden) comes in. Dataloaden is a Go package designed to streamline and optimize data loading in GraphQL servers. It generates Go code for dataloaders, which are a crucial part of GraphQL query performance. With dataloaden, you can efficiently load related data, minimize database queries, and provide faster responses to GraphQL queries. It's a popular tool for enhancing the performance of your GraphQL APIs and ensuring a smooth experience for clients when handling complex queries.

With the concept of what the dataloaden library accomplishes established, we can now explore how we put it to practical use within our project. In our dataloaders package, we utilize the generated Loader struct to create data loaders.

Begin by creating a new package under `internal` called `dataloaders` and create a new file called `dataloders.go` which will contain the following go code to create a new `IngredientLoader` implementation.

```go
package dataloaders

//go:generate go run github.com/vektah/dataloaden IngredientLoader string *internal/graph/model.Ingredient

// Loaders - directory for all the data loaders to be stored in context
type Loaders struct {
	// Ingredient - dataloader responsible for fetching ingredient objects
	Ingredient IngredientLoader
}
```

The `//go:generate` statement is a special comment in Go source code that is used to trigger code generation. In this code snippet, it is instructing the Go toolchain to run the specified go run command when code generation is needed.

In this specific case, the `//go:generate` statement is running the dataloaden command from the `github.com/vektah/dataloaden` package. This command generates code for a data loader named `IngredientLoader`. The data loader is responsible for efficiently fetching ingredient objects based on keys of type string. It is generated to work with the `internal/graph/model.Ingredient` model.

Additionally, the code defines a `Loaders` struct, which serves as a directory to store all data loaders within the context of the GraphQL server. This allows easy access to various data loaders for efficient data retrieval in GraphQL resolvers.

To integrate the dataloaders with our request handling process, we employ a middleware mechanism. The `DataloaderMiddleware` function, designed as a Gin middleware, serves as a pivotal bridge. This middleware extracts the necessary services and creates instances of dataloaders, such as our `IngredientLoader`, utilizing the `dataloaders.Loaders` struct. 

```go
func DataloaderMiddleware() gin.HandlerFunc{
    return func(c *gin.Context) {
		services := ForServices(c.Request.Context())
		loaders := dataloaders.Loaders{
			Ingredient: *dataloaders.NewIngredientLoader(dataloaders.IngredientLoaderConfig{
				Wait:     waitTime,
				MaxBatch: maxBatch,
				Fetch: func(ids []string) ([]*apimodel.Ingredient, []error) {
					ingredients := make([]*apimodel.Ingredient, len(ids))
					errors := make([]error, len(ids))
					for i, id := range ids {
						ingredient, err := services.IngredientService.FetchIngredient(id)
						if err != nil {
							errors[i] = err
							continue
						}
						ingredients[i] = ingredient
					}
					return ingredients, errors
				},
			}),
		}
		c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), loadersKey, &loaders))
		c.Next()
    }
}

// ForLoader - fetches the loader directory from context
func ForLoader(ctx context.Context) *dataloaders.Loaders {
	return ctx.Value(loadersKey).(*dataloaders.Loaders)
}
```

As the middleware associates the dataloaders with the request context, it ensures that our resolvers can efficiently utilize these dataloaders to retrieve data when needed.

In this manner, our GraphQL server can seamlessly harness the power of dataloaders to optimize its performance, making batched data retrieval a streamlined process. This middleware-driven approach ensures that the dataloaders are readily available throughout the request lifecycle, creating a high-performance GraphQL server ready to serve client requests efficiently.

## Summary

In conclusion, our journey through GraphQL integration and optimization has provided a holistic view of how to build a powerful and efficient GraphQL API in a Go project. We began by setting up gqlgen and customizing it to follow project conventions, ensuring a seamless integration. Defining the GraphQL schema and implementing resolvers brought our API to life, while the abstraction of the data model into services enhanced flexibility and maintainability. Middleware facilitated the integration of services, while built-in schema validation ensured the integrity of incoming requests. Finally, we tackled performance optimization with dataloaders, effectively mitigating the N+1 query problem. These topics, when combined, have equipped us with the knowledge and tools to create a robust, high-performance GraphQL server that aligns with Go project conventions. As we move forward, we'll explore additional facets of API development, building upon the strong foundation we've established. So stay tuned for more exciting insights in the next installment of our journey.

In our next post, we'll explore integrating GraphQL with the HTTP server Gin, and we'll enhance server security by implementing Keycloak as an authentication service.

## References

- gqlgen - <https://gqlgen.com/>
- Using GraphQL with Golang - <https://www.apollographql.com/blog/graphql/golang/using-graphql-with-golang/>
- Custom GraphQL validation - <https://david-yappeter.medium.com/gqlgen-custom-data-validation-part-1-7de8ef92de4c>
- Creating an Opinionated GraphQL Server with Go - <https://dev.to/cmelgarejo/creating-an-opinionated-graphql-server-with-go-part-2-46io>