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
Next, let's initialize our project using gqlgen
:
go run github.com/99designs/gqlgen init
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
andschema.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
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 calledtypes_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 typeInt
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!
}
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
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
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
}
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
}
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))
}
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 }
To run the server, use the following command:
go run server.go
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)
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?