Skip to content

forwardsoftware/demo-petstore-gql

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Petstore GraphQL Demo

An implementation of the Petstore sample from OpenAPI documentation to show how to generate a GraphQL backend with Go starting from schemas.


Table of Contents


What are we using?

Go

An open-source programming language supported by Google.

source: go.dev

GraphQL

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data.

source: graphql.org

PostgreSQL

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

Let's Code

Initialize Project

Create an empty Go Project

$ mkdir petstore-gql

$ cd petstore-gql

$ go mod init petstore-gql

GraphQL API

gqlgen

A Go library for building GraphQL servers without any fuss using a "Schema-first" approach.

source: gqlgen.com

Setup gqlgen

  • Add github.com/99designs/gqlgen to your project’s tools.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

Create GraphQL Schema

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!
}

Generate Go code

$ go run github.com/99designs/gqlgen generate
[Bonus] Automate Go code generation
  • At the top of resolver.go, between package and import, 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 ./...
Review generated Go code

Let's review the autogenerated Go code

  • resolver.go is the main dependency inject point for the GraphQL resolvers

    package 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 Mutations

    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.
    // 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 logic
    package 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.

PostgreSQL

Setup DB connection

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 function
    func 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()

PostgresSQL - goose

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

Install goose

$ go install github.com/pressly/goose/v3/cmd/goose@latest

Setup goose migrations execution

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)
      }

Create migrations folder

$ mkdir migrations

Create a migration

$ cd migrations

$ goose create pets_table_create sql
Review generated SQL migration
-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query';
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd

Implement custom SQL migration

-- +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

PostgresSQL - sqlc

sqlc generates fully type-safe idiomatic code from SQL.

  1. You write SQL queries
  2. You run sqlc to generate code that presents type-safe interfaces to those queries
  3. 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

Install sqlc

$ go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest

Init sqlc project

$ sqlc init
Review generated sqlc configuration
version: "2"
cloud:
    organization: ""
    project: ""
    hostname: ""
servers: []
sql: []
overrides:
    go: null
plugins: []
rules: []
options: {}

Update sqlc configuration

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

Implement SQL queries

  • Create a new folder to store queries
    $ mkdir db
  • Create queries.sql file inside db 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;
    

Generate Go code from SQL

$ sqlc generate

Connect SQL queries to code

Edit server.go file to use sqlc-generated code

  • Import generated db package
    import (
      //...
      dbqueries "petstore-gql/db"
      //...
    )
  • Create queries bindings passing existing db connection
      // Setup DB queries
      queries := dbqueries.New(db)

Put it all together

Setup queries injection into GraphQL resolvers

Edit resolver.go file

  • Add required import
    import (
        "petstore-gql/db"
    )
  • Add q field to Resolver struct
    type Resolver struct {
      q *db.Queries
    }
  • Add initializer method
    func NewRootResolvers(q *db.Queries) Config {
      return Config{
        Resolvers: &Resolver{
          q: q,
        },
      }
    }

Update entrypoint

Edit server.go and inject queries into RootResolvers

srv := handler.New(graph.NewExecutableSchema(graph.NewRootResolvers(queries)))

Implement "glue" code

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
}

Enjoy!

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