An implementation of the Petstore sample from OpenAPI documentation to show how to generate a GraphQL backend with Go starting from schemas.
- What are we using?
- Let's Code
An open-source programming language supported by Google.
source: go.dev
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data.
source: graphql.org
PostgreSQL is a powerful, open source object-relational database system with over 35 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance.
source: postgresql.org
$ mkdir petstore-gql
$ cd petstore-gql
$ go mod init petstore-gql
A Go library for building GraphQL servers without any fuss using a "Schema-first" approach.
source: gqlgen.com
-
Add
github.com/99designs/gqlgen
to your project’stools.go
$ printf '//go:build tools\npackage tools\nimport (_ "github.com/99designs/gqlgen"\n _ "github.com/99designs/gqlgen/graphql/introspection")' | gofmt > tools.go $ go mod tidy
-
Initialise
gqlgen
config and generate models$ go run github.com/99designs/gqlgen init $ go mod tidy
We now port the sample petstore
OpenAPI definition from the official documentation at examples/v3.0/petstore.html to a GraphQL schema
# Petstore demo GraphQL schema
#
# Ported from YAML schema at https://learn.openapis.org/examples/v3.0/petstore.html#yaml
type Pet {
id: ID!
name: String!
tags: [String!]
}
type Query {
"""
List all pets
limit: How many Pets to return at one time (max 100)
"""
pets(limit: Int): [Pet]!
"""
Info for a specific pet
id: The ID of the Pet to retrieve
"""
pet(id: ID!): Pet!
}
input PetInput {
name: String!
tags: [String!]
}
type Mutation {
"""
Create a pet
"""
petCreate(pet: PetInput!): Pet!
}
$ go run github.com/99designs/gqlgen generate
-
At the top of
resolver.go
, betweenpackage
andimport
, add the following line://go:generate go run github.com/99designs/gqlgen generate
-
To run
go generate
recursively over the entire project, we'll use this command:$ go generate ./...
Let's review the autogenerated Go code
-
resolver.go
is the main dependency inject point for the GraphQL resolverspackage graph //go:generate go run github.com/99designs/gqlgen generate // This file will not be regenerated automatically. // // It serves as dependency injection for your app, add any dependencies you require here. type Resolver struct{}
-
schema.resolvers.go
contains the generated GraphQL field resolvers for Queries and Mutationspackage 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. // Code generated by github.com/99designs/gqlgen version v0.17.66 import ( "context" "fmt" "petstore-gql/graph/model" ) // PetCreate is the resolver for the petCreate field. func (r *mutationResolver) PetCreate(ctx context.Context, pet model.PetInput) (*model.Pet, error) { panic(fmt.Errorf("not implemented: PetCreate - petCreate")) } // Pets is the resolver for the pets field. func (r *queryResolver) Pets(ctx context.Context, limit *int32) ([]*model.Pet, error) { panic(fmt.Errorf("not implemented: Pets - pets")) } // Pet is the resolver for the pet field. func (r *queryResolver) Pet(ctx context.Context, id string) (*model.Pet, error) { panic(fmt.Errorf("not implemented: Pet - pet")) } // Mutation returns MutationResolver implementation. func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } // Query returns QueryResolver implementation. func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver }
Note
By default gqlgen
will create stub resolvers that throw a "Not Implemented" error.
server.go
contains the main server setup and the HTTP logicpackage main import ( "log" "net/http" "os" "petstore-gql/graph" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/handler/extension" "github.com/99designs/gqlgen/graphql/handler/lru" "github.com/99designs/gqlgen/graphql/handler/transport" "github.com/99designs/gqlgen/graphql/playground" "github.com/vektah/gqlparser/v2/ast" ) const defaultPort = "8080" func main() { port := os.Getenv("PORT") if port == "" { port = defaultPort } srv := handler.New(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{}})) srv.AddTransport(transport.Options{}) srv.AddTransport(transport.GET{}) srv.AddTransport(transport.POST{}) srv.SetQueryCache(lru.New[*ast.QueryDocument](1000)) srv.Use(extension.Introspection{}) srv.Use(extension.AutomaticPersistedQuery{ Cache: lru.New[string](100), }) 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)) }
Note
In this file we also finds the setup of an HTTP server with all related middlewares and options.
Edit server.go
to setup Database connection using database/sql
standard library and lib/pq
- Import required libraries
package main import ( "database/sql" // ... _ "github.com/lib/pq" // ... )
- Add a
connectDatabase
functionfunc connectDatabase() (*sql.DB, error) { dbURL := os.Getenv("DATABASE_URL") if dbURL == "" { return nil, fmt.Errorf("missing DATABASE_URL environment variable") } db, err := sql.Open("postgres", dbURL) if err != nil { return nil, err } if err := db.Ping(); err != nil { return nil, err } return db, nil }
- invoke the function during server initialization
// Connect to DB db, err := connectDatabase() if err != nil { panic(err) } defer db.Close()
Goose is a database migration tool. Both a CLI and a library.
Manage your database schema by creating incremental SQL changes or Go functions.
source: goose doc
$ go install github.com/pressly/goose/v3/cmd/goose@latest
Edit server.go
to load and execute DB migrations using goose
library
- Import required dependencies
import ( //... _ "github.com/lib/pq" "github.com/pressly/goose/v3" //... )
- Setup migrations embedding
const defaultPort = "8080" //go:embed migrations/*.sql var embedMigrations embed.FS func main() {
- Setup
goose
dialect and migrations execution// Setup goose library goose.SetBaseFS(embedMigrations) // Setup goose dialect if err := goose.SetDialect("postgres"); err != nil { panic(err) } // Run migrations if err := goose.Up(db, "migrations"); err != nil { panic(err) }
$ mkdir migrations
$ cd migrations
$ goose create pets_table_create sql
-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query';
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
-- +goose Up
-- +goose StatementBegin
CREATE TABLE "ps_pets" (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
tags VARCHAR(255) []
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS "ps_pets";
-- +goose StatementEnd
sqlc generates fully type-safe idiomatic code from SQL.
- You write SQL queries
- You run sqlc to generate code that presents type-safe interfaces to those queries
- You write application code calling the methods sqlc generated.
Seriously, it's that easy. You don't have to write any boilerplate SQL querying code ever again.
source: sqlc.dev
$ go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
$ sqlc init
version: "2"
cloud:
organization: ""
project: ""
hostname: ""
servers: []
sql: []
overrides:
go: null
plugins: []
rules: []
options: {}
version: "2"
sql:
- engine: "postgresql"
queries: "db/queries.sql"
schema: "migrations"
gen:
go:
package: "db"
out: "db"
sql_package: "database/sql" # evaluate switch to "pgx/v5"
sql_driver: "github.com/lib/pq"
query_parameter_limit: 10
emit_empty_slices: true
- Create a new folder to store queries
$ mkdir db
- Create
queries.sql
file insidedb
folder-- name: PetsList :many SELECT * FROM ps_pets ORDER BY name LIMIT $1; -- name: PetCreate :one INSERT INTO ps_pets (name, tags) VALUES ($1, $2) RETURNING *; -- name: PetGet :one SELECT * FROM ps_pets WHERE id = $1;
$ sqlc generate
Edit server.go
file to use sqlc
-generated code
- Import generated
db
packageimport ( //... dbqueries "petstore-gql/db" //... )
- Create
queries
bindings passing existingdb
connection// Setup DB queries queries := dbqueries.New(db)
Edit resolver.go
file
- Add required import
import ( "petstore-gql/db" )
- Add
q
field toResolver
structtype Resolver struct { q *db.Queries }
- Add initializer method
func NewRootResolvers(q *db.Queries) Config { return Config{ Resolvers: &Resolver{ q: q, }, } }
Edit server.go
and inject queries
into RootResolvers
srv := handler.New(graph.NewExecutableSchema(graph.NewRootResolvers(queries)))
Update schema.resolvers.go
file to use injected queries
in resolvers.
// PetCreate is the resolver for the petCreate field.
func (r *mutationResolver) PetCreate(ctx context.Context, pet model.PetInput) (*model.Pet, error) {
pt, err := r.q.PetCreate(ctx, pet.Name, pet.Tags)
if err != nil {
return nil, err
}
return &model.Pet{
ID: fmt.Sprintf("%d", pt.ID),
Name: pt.Name,
Tags: pt.Tags,
}, nil
}
// Pets is the resolver for the pets field.
func (r *queryResolver) Pets(ctx context.Context, limit *int32) ([]*model.Pet, error) {
lmt := int32(100)
if limit != nil {
lmt = *limit
}
pts, err := r.q.PetsList(ctx, lmt)
if err != nil {
return nil, err
}
pets := make([]*model.Pet, 0, len(pts))
for _, pt := range pts {
pets = append(pets, &model.Pet{
ID: fmt.Sprintf("%d", pt.ID),
Name: pt.Name,
Tags: pt.Tags,
})
}
return pets, nil
}
// Pet is the resolver for the pet field.
func (r *queryResolver) Pet(ctx context.Context, id string) (*model.Pet, error) {
// string to int
iid, err := strconv.Atoi(id)
if err != nil {
return nil, err
}
pt, err := r.q.PetGet(ctx, int32(iid))
if err != nil {
return nil, err
}
return &model.Pet{
ID: fmt.Sprintf("%d", pt.ID),
Name: pt.Name,
Tags: pt.Tags,
}, nil
}
Start the server by running the following command
$ go run server.go
and open http://localhost:8080
to access the GraphiQL UI for our simple backend service.
Made with ✨ & ❤️ by ForWarD Software and contributors