【GraphQL&Go】gqlgen basics and MySQL
There are many talks about the goodness of GraphQL, but I think that the question “How do you actually implement it in Go?” Can not be understood unless you move it by hand with a brief Get Started of gqlgen. So, I would like to write about how to implement GraphQL with Go!

Basic configuration of graphql
To understand the basic structure of graphql, let’s take a look at the following flow. As the file grows larger and larger, it becomes difficult to imagine the whole picture, so let’s look at the contents from the very first simple structure.
1–1. Make a skeleton project
1–2. About each role of the generated file
1–1. Make a skeleton project
Following Getting Started, install gqlgen in Go’s project and create a skeleton project (template).
# create go module
$ mkdir gqlgen-todos
$ cd gqlgen-todos
$ go mod init github.com/[username]/gqlgen-todos# create skeleton project of gqlgen
$ go get github.com/99designs/gqlgen
$ go run github.com/99designs/gqlgen init
The Kelton project is created as follows:
├── go.mod
├── go.sum
├── gqlgen.yml // Code generation configuration file
├── graph
│ ├── generated // Automatically generated package
│ │ └── generated.go
│ ├── model // Package for graph model implemented in Go
│ │ └── models_gen.go
│ ├── resolver.go // Root resolver type definition file.
│ ├── schema.graphqls // GraphQL schema definition file.
│ └── schema.resolvers.go // resolver implementation file
└── server.go // Entry point to the app. Not overwritten by regeneration.
A template is very easily generated. This is the simple basic configuration of gqlgen. The root configuration is as follows.
- gqlgen.yml: configuration file
- graph: A package that contains various files used in the development of gqlgen (files here will be described later).
- server.go: main function
Next, let’s take a look at the role of each of the generated files.
1–2. About each role of the generated file
graph/schema.graphqls: GraphQL schema definition file
graph/schema.graphqls is a GraphQL schema definition file whose role is to manage API endpoints and their types. In the skeleton project, it is generated as follows. After that, I will add ⭐️ to the parts I added.
# ⭐️ type: Basic object type
type Todo {
id: ID!
text: String!
done: Boolean!
user: User!
}type User {
id: ID!
name: String!
}# ⭐️ type Query: Data fetch endpoint definition
type Query {
todos: [Todo!]!
}# ⭐️ input: Definition of object type for Mutaion
input NewTodo {
text: String!
userId: String!
}# ⭐️ type Mutation: Endpoint definition of the process to modify the data of Serverside
type Mutation {
createTodo (input: NewTodo!): Todo!
}
In gqlgen, basically edit this GraphQL definition file called graph/schema.graphqls and edit it
$ go run github.com/99designs/gqlgen generate
by executing the command, graph/model/models_gen.go and graph/schema.resolvers.go, which will be described later, will be regenerated and development will proceed, so this is the starting point for everything.
graph/model/models_gen.go: Go type definition file automatically generated from graph/schema.graphqls
graph/model/models_gen.go is a Go type definition file automatically generated from graph/schema.graphqls. As you can see by comparing with graph/schema.graphqls above, type Todo, type User and input NewTodo structs other than type Query and type Mutation are automatically generated.
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.package model// ⭐️ input File generated from New Todo
type NewTodo struct {
Text string `json:" text "`
UserID string `json:"userId "`
}// File generated from ⭐️ type Todo
type Todo struct {
ID string `json:" id "`
Text string `json:" text "`
Done bool `json:" done "`
User * User `json:" user "`
}// File generated from ⭐️ type User
type User struct {
ID string `json:" id "`
Name string `json:" name "` `
Friends [] * User `json:" friends "`
}
type Query and type Mutation are not created in graph/model/models_gen.go. It will be created in graph/schema.resolvers.go.
As it will be described in detail later, you can put your own Go model definition file *.go under graph/model. The Go model you defined is also loaded with the gqlgen command, and based on graph/schema.graphqls, the missing Fields in * .go are generated in graph/schema.resolvers.go. For now, remember that the graph/model package is responsible for managing the go structs that correspond to GraphQL type objects.
graph/schema.resolvers.go: Go file for endpoint implementation automatically generated from graph/schema.graphqls file
graph/schema.resolvers.go is an automatically generated Go file for endpoint implementation from the graph/schema.graphqls file. Endpoint management is your role. In the first state, the code is generated in the unimplemented state as shown below, so the flow is to implement it there.
The resolver has the same role as a common controller or handler, so you don’t have to think too hard. The detailed implementation in this can be implemented in resolver as you like, whether it is solid writing, MVC or DDD.
graph/schema.resolvers.go:
package graph// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.import (
"context"
"fmt"
"math / rand""github.com/shkomine/gqlgen-todos/graph/generated"
"github.com/shkomine/gqlgen-todos/graph/model"
)func (r * mutationResolver) CreateTodo (ctx context.Context, input model.NewTodo) (* model.Todo, error) {
// ⭐️ Implement data fetch here
// For example, this process
// r.todoRepo.Create (input)
panic (fmt.Errorf ("not implemented"))
}func (r * queryResolver) Todos (ctx context.Context) ([] * model.Todo, error) {
// ⭐️ Implement data changes here
// For example, this process
// r.todoRepo.List ()
panic (fmt.Errorf ("not implemented"))
}// Mutation returns generated. MutationResolver implementation.
func (r * Resolver) Mutation () generated.MutationResolver {return & mutationResolver {r}}// Query returns generated.QueryResolver implementation.
func (r * Resolver) Query () generated.QueryResolver {return & queryResolver {r}}type mutationResolver struct {* Resolver}
type queryResolver struct {* Resolver}
Now, take a look at the above mutationResolver and queryResolver code.
type mutationResolver struct {* Resolver}
type queryResolver struct {* Resolver}
Both contain Resolver type definitions, and state-bearing roles are aggregated in Resolver.
graph/resolver.go, server.go: go package not regenerated by gqlgen command
The type definition of Resolver is defined in graph/resolver.go, and its role is to centrally manage the state. The type cannot be renamed or deleted as it is referenced from the auto-generated graph/schema.resolvers.go, but this Resolver will not be regenerated by running the gqlgen command after initial generation. Since there is no such thing, this struct can have an instance (pointer) etc. that you want to keep referencing. If it is MVC, I think that it will be an implementation that has structs for each model, other repository, service, and use case.
graph/resolver.go:
package graphimport "github.com/shkomine/gqlgen-todos/graph/model"// This file will not be regenerated automatically.
// //
// It serves as dependency injection for your app, add any dependencies you require here.type Resolver struct {
// ⭐️ TODO: Hold an instance such as UserRepository here
}
It is server.go that creates the Resolver instance. Contains Go’s main function. This file is also not regenerated by executing the gqlgen command after it is first created with the skeleton object, so you can freely rewrite it.
package mainimport (
"log"
"net / http"
"os""github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/shkomine/gqlgen-todos/graph"
"github.com/shkomine/gqlgen-todos/graph/generated"
)const defaultPort = "8080"func main () {
port: = os.Getenv ("PORT")
if port == "" {
port = defaultPort
}// ⭐️ Create an instance of Resolver here
srv: = handler.NewDefaultServer (generated.NewExecutableSchema (generated.Config {Resolvers: & graph.Resolver {}}))// ⭐️ set graphql playground to `/ query`
http.Handle ("/", playground.Handler ("GraphQL playground", "/ query"))
http.Handle ("/ query", srv)log.Printf ("connect to http: // localhost:% s / for GraphQL playground", port)
log.Fatal (http.ListenAndServe (":" + port, nil))
}
So far, we have explained the basic configuration of gqlgen based on the initially generated skeleton project. If you want to get to the point where you can actually move it, it will finish quickly, so please try the official Getting Started. I think you can understand it more deeply.
Let’s connect GraphQL to MySQL
I have already prepared Database and users table with columns which contain id, username, email, password, created_at, is_deleted.
I rewrited schema.graphqls like:
# GraphQL schema example
# https://gqlgen.com/getting-started/scalar Timetype User {
id: ID!
username: String!
email: String!
password: String!
created_at: Time!
is_deleted: Boolean!
}type Query {
users: [User!]!
}input NewUser {
username: String!
email: String!
password: String!
}type Mutation {
createUser(input: NewUser!): User!
}
This for to create user when user signup! And need to run to generate files:
$ go run github.com/99designs/gqlgen generate
db folder and file is necessary to connect to database and use mysql.
$ go get github.com/go-sql-driver/mysql
db/connnection.go:
package dbimport (
"database/sql"
"fmt"
"os"_ "github.com/go-sql-driver/mysql"
)func ConnectDB() (*sql.DB, error) {
USERNAME := os.Getenv("USERNAME")
PASSWORD := os.Getenv("PASSWORD")
HOST := os.Getenv("HOST")
PORT := os.Getenv("PORT")
DATABASE := os.Getenv("DATABASE") dbconf := USERNAME + ":" + PASSWORD + "@tcp(" + HOST + ":" + PORT + ")/" + DATABASE + "?charset=utf8mb4" + "&parseTime=True" db, err := sql.Open(os.Getenv("DRIVER"), dbconf)
if err != nil {
fmt.Println("Error connecting to database : error=%v", err)
return nil, err
} return db, err
}
I added my db information to .env file.
.env:
DRIVER=mysql
DSN=user:password@tcp(db-dev:3306)/db_devHOST=YOUR_HOST
DATABASE=YOUR_DATABASE
USERNAME=YOUR_NAME
PASSWORD=YOUR_PASSWORD
PORT=YOUR_PORT
To read .env file, another package should be imported.
$ go get github.com/joho/godotenv
Then I rewrited server.go:
package mainimport (
"log"
"net/http"
"os" "github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/TIshow/learn-to-donate/graph"
"github.com/TIshow/learn-to-donate/graph/generated"
"github.com/joho/godotenv"
)func envLoad() {
err := godotenv.Load(".env")
if err != nil {
log.Fatalf("Error loading env target")
}
}const defaultPort = "3306"func main() {
envLoad() // Load from .env file port := os.Getenv("PORT")
if port == "" {
port = defaultPort
} srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}})) http.Handle("/", playground.Handler("GraphQL playground", "/query"))
http.Handle("/query", srv) log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
Now, ready for db connection!
graph/schema.resolvers.go needs to be edited to use SQL.
package graph// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.import (
"context"
"fmt"
"time" "github.com/TIshow/learn-to-donate/db"
"github.com/TIshow/learn-to-donate/graph/generated"
"github.com/TIshow/learn-to-donate/graph/model"
)func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
db, err := db.ConnectDB() // connect to DB.
if err != nil {
panic(err)
} else {
fmt.Println("Successful Connection to DB !")
} var user model.User
user.Username = input.Username
user.Email = input.Email
user.Password = input.Password
user.IsDeleted = false now := time.Now()
user.CreatedAt = now _, err = db.Exec(`INSERT INTO users (username, email, password, created_at, updated_at, is_deleted) VALUES (?, ?, ?, ?, ?, ?)`, user.Username, user.Email, user.Password, user.CreatedAt, user.UpdatedAt, user.IsDeleted)
if err != nil {
panic(err)
} else {
fmt.Println("Insert User is successed !")
} defer db.Close() return &user, nil
}func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
return r.users, nil
}// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
Function CreateUser make execution to insert New User.
db, err := db.ConnectDB() // connect to DB.
This is what I defined before in db directory to make connection to DB.
var user model.User
user.Username = input.Username
user.Email = input.Email
user.Password = input.Password
user.IsDeleted = false
GraphQL input from mutation is saved to model.User. This is what I defined in schema.graphqls and generated automatically.
_, err = db.Exec(`INSERT INTO users (username, email, password, created_at, updated_at, is_deleted) VALUES (?, ?, ?, ?, ?, ?)`, user.Username, user.Email, user.Password, user.CreatedAt, user.UpdatedAt, user.IsDeleted)
This is SQL.
Open GraphiQL(in my case, http://localhost:3306/)
mutation {
createUser(input: {username: "test", email: "test", password: "test"}){
id
username
email
password
created_at
is_deleted
}
}
If this mutation is succeeded, the input values would be inserted your BD table.
Conclusion
I was confused about what to write in which file and what to pass as an argument, but I’m glad I managed to do it.
The one that actually works is different from the name of the structure, so please let me know if it doesn’t work in the above example.
Happy coding !