DEV Community

Cover image for A Comprehensive Tutorial on Building a GraphQL API in Go using gqlgen and Gorm
Francisco Mendes
Francisco Mendes

Posted on • Edited on

A Comprehensive Tutorial on Building a GraphQL API in Go using gqlgen and Gorm

Introduction

In today's tutorial we are going to create a graphql api using the gqlgen framework that has a schema first approach, is type safe and still uses codegen to create data types and in our resolvers.

What I find most amazing about gqlgen is that the codegen is configurable, has very complete documentation, is easy to learn and is quite complete in terms of features.

Prerequisites

Before going further, you are expected to have basic knowledge of these technologies:

  • Go
  • GraphQL
  • ORM's

Getting Started

Our first step will be to create our project folder:

mkdir haumea
cd haumea
go mod init haumea
Enter fullscreen mode Exit fullscreen mode

Next, let's initialize our project using gqlgen:

go run github.com/99designs/gqlgen init
Enter fullscreen mode Exit fullscreen mode

Now we can make some changes regarding the structure of our project. Inside the graph/ folder we will delete the following folders and files:

  • model/ - this folder held the project's models/data types
  • resolver.go and schema.resolvers.go - these files are related to the implementation of schema resolvers
  • schema.graphqls - api/project graphql schema

After removing the files and folders we can now edit the gqlgen.yml file to add our configuration, which would be the following:

# @/gqlgen.yml
schema:
  - graph/typeDefs/*.gql

exec:
  filename: graph/generated/generated.go
  package: generated

model:
  filename: graph/customTypes/types_gen.go
  package: customTypes

resolver:
  layout: follow-schema
  dir: graph/resolvers
  package: graph
  filename_template: "{name}.resolvers.go"

autobind:

models:
  ID:
    model:
      - github.com/99designs/gqlgen/graphql.Int
      - github.com/99designs/gqlgen/graphql.ID
      - github.com/99designs/gqlgen/graphql.Int64
      - github.com/99designs/gqlgen/graphql.Int32
  Int:
    model:
      - github.com/99designs/gqlgen/graphql.Int
      - github.com/99designs/gqlgen/graphql.Int64
      - github.com/99designs/gqlgen/graphql.Int32
Enter fullscreen mode Exit fullscreen mode

In the above configuration, we made the following changes:

  • all our schema files will be inside the typeDefs folder and with the .gql extension
  • the models/data types will be stored inside the customTypes/ folder in a file called types_gen.go
  • our resolvers will be generated inside the resolvers/ folder
  • our identifiers (id) when using the graphql type ID will map to the golang data type Int

With this in mind we can now create the schema for today's project, first let's create the typeDefs/ folder and create the todo.gql file inside it:

# @/graph/typeDefs/todo.gql
type Todo {
  id: ID!
  text: String!
  done: Boolean!
}

input TodoInput {
  id: ID!
  text: String!
  done: Boolean!
}

type Mutation {
  createTodo(text: String!): Todo!
  updateTodo(input: TodoInput!): Todo!
  deleteTodo(todoId: ID!): Todo!
}

type Query {
  getTodos: [Todo!]!
  getTodo(todoId: ID!): Todo!
}
Enter fullscreen mode Exit fullscreen mode

With the schema created, the next step will be to generate the data types and resolvers for queries and mutations:

go run github.com/99designs/gqlgen generate
Enter fullscreen mode Exit fullscreen mode

Now that we have all this done, we can now move on to the next step which will be to establish the connection to a database:

go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
Enter fullscreen mode Exit fullscreen mode

Then we can create a folder called common/ with the database connection configuration in the db.go file:

// @/graph/common/db.go
package common

import (
    "haumea/graph/customTypes"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

func InitDb() (*gorm.DB, error) {
    var err error
    db, err := gorm.Open(sqlite.Open("dev.db"), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Silent),
    })
    if err != nil {
        return nil, err
    }
    db.AutoMigrate(&customTypes.Todo{})
    return db, nil
}
Enter fullscreen mode Exit fullscreen mode

With the database connection configured we can now create the graphql api context, still inside the common/ folder we will create the context.go file (in this article we will just pass the connection of our database, but you can extend context easily):

// @/graph/common/context.go
package common

import (
    "context"
    "net/http"

    "gorm.io/gorm"
)

type CustomContext struct {
    Database *gorm.DB
}

var customContextKey string = "CUSTOM_CONTEXT"

func CreateContext(args *CustomContext, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        customContext := &CustomContext{
            Database: args.Database,
        }
        requestWithCtx := r.WithContext(context.WithValue(r.Context(), customContextKey, customContext))
        next.ServeHTTP(w, requestWithCtx)
    })
}

func GetContext(ctx context.Context) *CustomContext {
    customContext, ok := ctx.Value(customContextKey).(*CustomContext)
    if !ok {
        return nil
    }
    return customContext
}
Enter fullscreen mode Exit fullscreen mode

Now we need to go to the api's entry file to start connecting to our database and pass the database in the context of our api. This way:

// @/server.go
package main

import (
    "haumea/common"
    "haumea/graph/generated"
    resolvers "haumea/graph/resolvers"
    "log"
    "net/http"
    "os"

    "github.com/99designs/gqlgen/graphql/handler"
    "github.com/99designs/gqlgen/graphql/playground"
)

const defaultPort = "4000"

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = defaultPort
    }

    db, err := common.InitDb()
    if err != nil {
        log.Fatal(err)
    }

    srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &resolvers.Resolver{}}))

    customCtx := &common.CustomContext{
        Database: db,
    }

    http.Handle("/", playground.Handler("GraphQL playground", "/query"))
    http.Handle("/query", common.CreateContext(customCtx, srv))

    log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}
Enter fullscreen mode Exit fullscreen mode

Now that we have the server configured, the database setup done and with the connection created, we just need to work on the resolvers.

In each of the resolvers, if we want to access the context we have created, we use the GetContext() function to which we will get the instance of our database to perform the CRUD.

// @/graph/resolvers/todo.resolvers.go
package graph

import (
    "context"
    "haumea/common"
    "haumea/graph/customTypes"
    "haumea/graph/generated"
)

func (r *mutationResolver) CreateTodo(ctx context.Context, text string) (*customTypes.Todo, error) {
    context := common.GetContext(ctx)
    todo := &customTypes.Todo{
        Text: text,
        Done: false,
    }
    err := context.Database.Create(&todo).Error
    if err != nil {
        return nil, err
    }
    return todo, nil
}

func (r *mutationResolver) UpdateTodo(ctx context.Context, input customTypes.TodoInput) (*customTypes.Todo, error) {
    context := common.GetContext(ctx)
    todo := &customTypes.Todo{
        ID:   input.ID,
        Text: input.Text,
        Done: input.Done,
    }
    err := context.Database.Save(&todo).Error
    if err != nil {
        return nil, err
    }
    return todo, nil
}

func (r *mutationResolver) DeleteTodo(ctx context.Context, todoID int) (*customTypes.Todo, error) {
    context := common.GetContext(ctx)
    var todo *customTypes.Todo
    err := context.Database.Where("id = ?", todoID).Delete(&todo).Error
    if err != nil {
        return nil, err
    }
    return todo, nil
}

func (r *queryResolver) GetTodos(ctx context.Context) ([]*customTypes.Todo, error) {
    context := common.GetContext(ctx)
    var todos []*customTypes.Todo
    err := context.Database.Find(&todos).Error
    if err != nil {
        return nil, err
    }
    return todos, nil
}

func (r *queryResolver) GetTodo(ctx context.Context, todoID int) (*customTypes.Todo, error) {
    context := common.GetContext(ctx)
    var todo *customTypes.Todo
    err := context.Database.Where("id = ?", todoID).Find(&todo).Error
    if err != nil {
        return nil, err
    }
    return todo, 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 }
Enter fullscreen mode Exit fullscreen mode

To run the server, use the following command:

go run server.go
Enter fullscreen mode Exit fullscreen mode

And to test each of the queries and mutations of this project, just open a new tab in the browser and visit the http://localhost:4000/.

Conclusion

As usual, I hope you enjoyed the article and that it helped you with an existing project or simply wanted to try it out.

If you found a mistake in the article, please let me know in the comments so I can correct it. Before finishing, if you want to access the source code of this article, I leave here the link to the github repository.

Top comments (1)

Collapse
 
andriykalashnykov profile image
Andriy Kalashnykov

Francisco, this is a great article. You took it more than one notch higher than rest of the crowd. Wondering if you came across slightly different context of GraphQL usage: I have REST API server written in Go(echo/gorm/swaggo) and looking for an advanced example on hot to utilize gqlgen to point to existing REST services and reuse existing models(structs) from another repo. Have you seen anything like this or have interest in collaborating on such example?