For the most part, my background in writing web APIs has been with Node.js and Ruby/Rails. I’m finding that this created a bit of a struggle when first trying to figure out designing web apis in Go. One article that was really helpful to me was Structuring Applications in Go by Ben Johnson. It has influenced some of the code in this tutorial and I recommend reading!
First things first, lets start by getting the project set up. I’ll be using macOS for the tutorial but for the most part that won’t matter. Here are links to setting up Go and PostgreSQL on mac if you don’t have them on your machine. Below is a link to the code in full for reference:
Create a new project - we’ll call it go-graphql-api
. To make it easier lets also create the whole project structure right away.
├── gql │ ├── gql.go │ ├── queries.go │ ├── resolvers.go │ └── types.go ├── main.go ├── postgres │ └── postgres.go └── server └── server.go
There are a couple of Go dependencies we’ll also want to grab. I like using realize for hot reloading while developing, chi and render from go-chi as a lightweight router and to help manage request/response payloads, and graphql-go/graphql as our GraphQL implementation.
go get github.com/oxequa/realizego get
Lastly, let’s set up a database and some mock data to be able to test against. Run psql
to enter the Postgres console and we’ll create a database.
CREATE DATABASE go_graphql_db;
Then we’ll connect to it:
\c go_graphql_db
Once connected, go ahead and paste the following SQL into the console.
CREATE TABLE users (
id serial PRIMARY KEY,
name VARCHAR (50) NOT NULL,
age INT NOT NULL,
profession VARCHAR (50) NOT NULL,
friendly BOOLEAN NOT NULL
);
INSERT INTO users VALUES
(1, 'kevin', 35, 'waiter', true),
(2, 'angela', 21, 'concierge', true),
(3, 'alex', 26, 'zoo keeper', false),
(4, 'becky', 67, 'retired', false),
(5, 'kevin', 15, 'in school', true),
(6, 'frankie', 45, 'teller', true);
We just created a basic users table and inserted five new users. This will do just fine for the purpose of this walkthrough. We should now be good to start building out our API!
Throughout this post all of the code snippets will include a fair bit of comments within them to help with explaining things step by step.
We’ll start with main.go
.
package main
import (
"fmt"
"log"
"net/http"
"github.com/bradford-hamilton/go-graphql-api/gql"
"github.com/bradford-hamilton/go-graphql-api/postgres"
"github.com/bradford-hamilton/go-graphql-api/server"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/render"
"github.com/graphql-go/graphql"
)
func main() {
// Initialize our api and return a pointer to our router for http.ListenAndServe
// and a pointer to our db to defer its closing when main() is finished
router, db := initializeAPI()
defer db.Close()
// Listen on port 4000 and if there's an error log it and exit
log.Fatal(http.ListenAndServe(":4000", router))
}
func initializeAPI() (*chi.Mux, *postgres.Db) {
// Create a new router
router := chi.NewRouter()
// Create a new connection to our pg database
db, err := postgres.New(
postgres.ConnString("localhost", 5432, "bradford", "go_graphql_db"),
)
if err != nil {
log.Fatal(err)
}
// Create our root query for graphql
rootQuery := gql.NewRoot(db)
// Create a new graphql schema, passing in the the root query
sc, err := graphql.NewSchema(
graphql.SchemaConfig{Query: rootQuery.Query},
)
if err != nil {
fmt.Println("Error creating schema: ", err)
}
// Create a server struct that holds a pointer to our database as well
// as the address of our graphql schema
s := server.Server{
GqlSchema: &sc,
}
// Add some middleware to our router
router.Use(
render.SetContentType(render.ContentTypeJSON), // set content-type headers as application/json
middleware.Logger, // log api request calls
middleware.DefaultCompress, // compress results, mostly gzipping assets and json
middleware.StripSlashes, // match paths with a trailing slash, strip it, and continue routing through the mux
middleware.Recoverer, // recover from panics without crashing server
)
// Create the graphql route with a Server method to handle it
router.Post("/graphql", s.GraphQL())
return router, db
}
Keep in mind the import paths up top for your local gql
, postgres
, and server
packages will be different (path will include your name not mine) as well as the Postgres user you pass into postgres.ConnString()
. There are a few different things going on inside of initializeAPI()
so we’ll go through step by step and build out each piece.
The router is created with chi.NewRouter()
which returns a mux for us. The next few lines we are creating a new connection to our Postgres database. We build a connection string with postgres.ConnString()
and pass it into postgres.New()
. This is our package code so let’s build that out! We’ll hop over to postgres.go
.
package postgres
import (
"database/sql"
"fmt"
// postgres driver
_ "github.com/lib/pq"
)
// Db is our database struct used for interacting with the database
type Db struct {
*sql.DB
}
// New makes a new database using the connection string and
// returns it, otherwise returns the error
func New(connString string) (*Db, error) {
db, err := sql.Open("postgres", connString)
if err != nil {
return nil, err
}
// Check that our connection is good
err = db.Ping()
if err != nil {
return nil, err
}
return &Db{db}, nil
}
// ConnString returns a connection string based on the parameters it's given
// This would normally also contain the password, however we're not using one
func ConnString(host string, port int, user string, dbName string) string {
return fmt.Sprintf(
"host=%s port=%d user=%s dbname=%s sslmode=disable",
host, port, user, dbName,
)
}
// User shape
type User struct {
ID int
Name string
Age int
Profession string
Friendly bool
}
// GetUsersByName is called within our user query for graphql
func (d *Db) GetUsersByName(name string) []User {
// Prepare query, takes a name argument, protects from sql injection
stmt, err := d.Prepare("SELECT * FROM users WHERE name=$1")
if err != nil {
fmt.Println("GetUserByName Preperation Err: ", err)
}
// Make query with our stmt, passing in name argument
rows, err := stmt.Query(name)
if err != nil {
fmt.Println("GetUserByName Query Err: ", err)
}
// Create User struct for holding each row's data
var r User
// Create slice of Users for our response
users := []User{}
// Copy the columns from row into the values pointed at by r (User)
for rows.Next() {
err = rows.Scan(
&r.ID,
&r.Name,
&r.Age,
&r.Profession,
&r.Friendly,
)
if err != nil {
fmt.Println("Error scanning rows: ", err)
}
users = append(users, r)
}
return users
The general idea here is that we provide the ability to create a new database which returns a Db
struct holding the connection. We then write GetUserByUsername()
as a method on Db
. As you can tell the actual functionality/query is pretty useless, but the point is to get everything wired up as a good starting point.
Our next concern back in main.go
is on line 40 where we’re creating a a root query that will get used to create our GraphQL schema. Let’s jump into queries.go
inside our gql
package.
package gql
import (
"github.com/bradford-hamilton/go-graphql-api/postgres"
"github.com/graphql-go/graphql"
)
// Root holds a pointer to a graphql object
type Root struct {
Query *graphql.Object
}
// NewRoot returns base query type. This is where we add all the base queries
func NewRoot(db *postgres.Db) *Root {
// Create a resolver holding our databse. Resolver can be found in resolvers.go
resolver := Resolver{db: db}
// Create a new Root that describes our base query set up. In this
// example we have a user query that takes one argument called name
root := Root{
Query: graphql.NewObject(
graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"users": &graphql.Field{
// Slice of User type which can be found in types.go
Type: graphql.NewList(User),
Args: graphql.FieldConfigArgument{
"name": &graphql.ArgumentConfig{
Type: graphql.String,
},
},
Resolve: resolver.UserResolver,
},
},
},
),
}
return &root
}
We pass our db
into NewRoot()
and then create a Resolver
with it. This way all of theResolver
methods have access to the database. We then create a new root that has a users
query. It takes a an argument called name
and is of type graphql.NewList
of User
(slice/array of User) which we’ve moved out into types.go
inside of the gql
package. This is where we could start to grow our root to hold many different kinds of queries. Remember to change the import path for your local postgres
package to your own. Let’s take a look at resolvers.go
.
package gql
import (
"github.com/bradford-hamilton/go-graphql-api/postgres"
"github.com/graphql-go/graphql"
)
// Resolver struct holds a connection to our database
type Resolver struct {
db *postgres.Db
}
// UserResolver resolves our user query through a db call to GetUserByName
func (r *Resolver) UserResolver(p graphql.ResolveParams) (interface{}, error) {
// Strip the name from arguments and assert that it's a string
name, ok := p.Args["name"].(string)
if ok {
users := r.db.GetUsersByName(name)
return users, nil
}
return nil, nil
}
Same thing again here with updating the postgres
import. Here is where we would start adding more resolver methods when we need them. Let’s open up types.go
.
package gql
import "github.com/graphql-go/graphql"
// User describes a graphql object containing a User
var User = graphql.NewObject(
graphql.ObjectConfig{
Name: "User",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.Int,
},
"name": &graphql.Field{
Type: graphql.String,
},
"age": &graphql.Field{
Type: graphql.Int,
},
"profession": &graphql.Field{
Type: graphql.String,
},
"friendly": &graphql.Field{
Type: graphql.Boolean,
},
},
},
)
In a similar fashion, here is where we would start adding all of our different types. As you can see within each field we specify the type as well. Now if you look on line 42 back in main.go
we create a new schema with our root query.
Down a little further on line 51 we create a new server. It holds a pointer to our GraphQL schema. Let’s check out server.go
.
package server
import (
"encoding/json"
"net/http"
"github.com/bradford-hamilton/go-graphql-api/gql"
"github.com/go-chi/render"
"github.com/graphql-go/graphql"
)
// Server will hold connection to the db as well as handlers
type Server struct {
GqlSchema *graphql.Schema
}
type reqBody struct {
Query string `json:"query"`
}
// GraphQL returns an http.HandlerFunc for our /graphql endpoint
func (s *Server) GraphQL() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Check to ensure query was provided in the request body
if r.Body == nil {
http.Error(w, "Must provide graphql query in request body", 400)
return
}
var rBody reqBody
// Decode the request body into rBody
err := json.NewDecoder(r.Body).Decode(&rBody)
if err != nil {
http.Error(w, "Error parsing JSON request body", 400)
}
// Execute graphql query
result := gql.ExecuteQuery(rBody.Query, *s.GqlSchema)
// render.JSON comes from the chi/render package and handles
// marshalling to json, automatically escaping HTML and setting
// the Content-Type as application/json.
render.JSON(w, r, result)
}
}
Here we have a method on our server called GraphQL
. For all intents and purposes this will be the only method needed to handle GraphQL queries. Reminder: update the import path for gql
. The last file we need to look at is gql.go
.
package gql
import (
"fmt"
"github.com/graphql-go/graphql"
)
// ExecuteQuery runs our graphql queries
func ExecuteQuery(query string, schema graphql.Schema) *graphql.Result {
result := graphql.Do(graphql.Params{
Schema: schema,
RequestString: query,
})
// Error check
if len(result.Errors) > 0 {
fmt.Printf("Unexpected errors inside ExecuteQuery: %v", result.Errors)
}
return result
}
This simply holds the function ExecuteQuery()
which runs our GraphQL queries. I imagined that in here is where we would create something like an ExecuteMutation()
type function as well to handle GraphQL mutations.
At the end of initializeAPI()
we add some middleware to the router and mount our GraphQL
server method to handle POSTs to /graphql
. If any RESTful routes were needed, they could be mounted here and methods for handling them could be added to Server
.
Go ahead and run realize init
in the root of the project. You should be prompted twice and respond with n
both times.
This creates the .realize.yaml
file in the root of your project. Let’s go ahead and overwrite it with:
settings:
legacy:
force: false
interval: 0s
schema:
- name: go-graphql-api
path: .
commands:
run:
status: true
watcher:
extensions:
- go
paths:
- /
ignored_paths:
- .git
- .realize
- vendor
The important piece of this config is that it will watch for any changes within the project and when it detects one, it will automatically restart the server and rerun main.go
.
There are some great tools for exploring your GraphQL API like graphiql, insomnia, and graphql-playground. You can also just make a POST request sending over a raw application/json body like this:
{
"query": "{users(name:\"kevin\"){id, name, age}}"
}
In Postman it would look something like this:
Go ahead and ask for just one, or any combination of attributes in your query. In true GraphQL fashion we can request just the information we want sent over the wire.
That’s it! Hopefully this has been useful to help get you off the ground with writing a GraphQL API in Go. I tried to break it down into it’s package/file layout so that it had room to grow/scale, for clarity, and to make testing each piece easier. Please feel free to leave comments if you have any recommendations or questions!
Recommended Courses:
☞ Persistence with mongoDB in Go(golang)
☞ Go: The Complete Developer’s Guide (Golang)
☞ Google Go Programming for Beginners (golang)
☞ Getting started with Cloud Native Go
☞ Advanced Google’s Go (golang) Programming Course
☞ Learn GraphQL with Laravel and Vue.js - Full Tutorial
☞ Build A Restful Api With Node.js Express & MongoDB | Rest Api Tutorial
☞ Making It All Fit Together with React and GraphQL
☞ APIs for Beginners - How to use an API (Full Course)