Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
25 Oct 2025 · 5 min read ·Article 117 / 125
Go

117 Creating Custom Directives in gqlgen

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

117 Creating Custom Directives in gqlgen

If you have spent any meaningful time working with GraphQL in the Go ecosystem, chances are you are already familiar with gqlgen. This library has become the de facto standard for building GraphQL servers in Go. Its explicit and strongly typed nature gives it an edge when it comes to maintainable and debuggable code. There is, however, one feature that often goes underused: Custom Directives.

In this 117th article, I will walk you through building custom directives in gqlgen. We will not only cover the code, but also the technical reasoning behind it, the request flow, a simulation, and several best practices I have picked up while implementing this feature across a number of production systems.


1. Why Custom Directives Matter

A directive in GraphQL is essentially an annotation that we can attach to the schema or a query to give specific instructions while a resolver runs. Some examples of built-in directives are @skip and @include. Often, though, we need specific business logic—such as authorization, logging, input validation, or data masking. This is exactly where custom directives come into play.


2. Directive Execution Flow in gqlgen

Before diving into the code, let’s first understand how gqlgen executes a directive. The flow looks roughly like this:

MERMAID
flowchart TD
  A[Client Request] --> B(GraphQL Server Parse Query)
  B --> C{Ada Directive?}
  C -- Ya --> D[Eksekusi Logic Directive]
  D --> E[Eksekusi Resolver]
  C -- Tidak --> E
  E --> F[Return Response ke Client]

Notes:

  • Every field/type in the schema that uses a directive is processed by the directive’s middleware function before the main resolver is executed.
  • The logic inside a directive can either halt the request or let it continue.

3. Case Study: Building an @auth(role: "ADMIN") Directive

Problem

Suppose you want a certain field to be accessible only to users with the ADMIN role. We want a schema like this:

graphql
1type Query {
2  users: [User!]! @auth(role: "ADMIN")
3}
4
5type User {
6  id: ID!
7  name: String!
8}

4. Defining the Directive in the Schema

First, we modify schema.graphql:

graphql
1directive @auth(
2  role: String!
3) on FIELD_DEFINITION

5. Generating the Directive Code

After changing the schema, run:

sh
1go run github.com/99designs/gqlgen generate

This adds a stub to generated.go and leaves us to provide the implementation in resolver.go or another module.


6. Implementing the Directive Logic

Prepare the User Context

Authentication usually stores user information in context.Context. For example, in an HTTP middleware:

go
 1type User struct {
 2    ID   string
 3    Role string
 4}
 5
 6func AuthMiddleware(next http.Handler) http.Handler {
 7    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 8        // Simulation: the user is already logged in as either "USER" or "ADMIN"
 9        user := &User{ID: "1", Role: r.Header.Get("X-Role")}
10        ctx := context.WithValue(r.Context(), "user", user)
11        next.ServeHTTP(w, r.WithContext(ctx))
12    })
13}

Implementing the @auth Directive

Open resolver.go and locate (or create):

go
 1func (r *directiveResolver) Auth(ctx context.Context, obj interface{}, next graphql.Resolver, role string) (res interface{}, err error) {
 2    userVal := ctx.Value("user")
 3    user, ok := userVal.(*User)
 4    if !ok || user == nil {
 5        return nil, fmt.Errorf("unauthenticated")
 6    }
 7
 8    if user.Role != role {
 9        return nil, fmt.Errorf("unauthorized: need role %s", role)
10    }
11
12    // Continue to the next resolver
13    return next(ctx)
14}

In short, the code above:

  • Retrieves the user from the context.
  • If the user is not logged in or the role does not match, returns an error.
  • If the check passes, calls next(ctx) so the resolver runs.

7. Simulation and Testing

Let’s create a simple HTTP test. The GraphQL endpoint is wrapped with the AuthMiddleware.

Example HTTP Request

http
1POST /graphql
2Headers:
3  X-Role: USER
4Body:
5  {
6    "query": "{ users { id name } }"
7  }
Result: Unauthorized error.

http
1POST /graphql
2Headers:
3  X-Role: ADMIN
4Body:
5  {
6    "query": "{ users { id name } }"
7  }
Result: Success, the user list is returned.


8. Execution Flow Table

HTTP HeaderDirective TriggerResultNotes
X-Role: USER@auth(“ADMIN”)ErrorRejected, role does not match
X-Role:@auth(“ADMIN”)ErrorRejected, not logged in
X-Role: ADMIN@auth(“ADMIN”)SuccessPasses, resolver is executed
-NoneSuccessResolver is executed normally

9. Best Practices for Custom Directives in gqlgen

  • Isolate the Logic: Keep the directive logic in its own package (internal/directive/) so it is easy to unit test.
  • Reusable: Use the context and type assertions; avoid global variables.
  • Error Handling: Return a custom error type so it is easy to distinguish on the frontend.
  • Composable: You can chain more than one directive, and the order in the schema matters.

10. Multi-Directive Simulation

Suppose you want a field protected by both @auth and @log.

graphql
1type Query {
2  sensitiveInfo: String! @auth(role: "ADMIN") @log
3}

gqlgen executes from left to right: @auth first, then @log, and finally the main resolver.


11. Closing & Reflection

With custom directives in gqlgen, you can move common logic such as authorization, auditing, and more into the schema layer. This clarifies the separation of concerns and makes the schema truly self-documenting.

Implementing it is not difficult, but the key to success in production lies in consistency, testing, and a robust context design. The implementation above is just a simple example—you can certainly extend it for the needs of your organization, such as dynamic permissions, rate limiting, or automatic data masking.

Happy experimenting with custom directives in gqlgen—may your GraphQL architecture grow ever more robust and maintainable!


References:


Critiques, suggestions, and discussion are very welcome in the comments. See you in the next article!

Related Articles

💬 Comments