Serverless with Go and Graphql

Timothy White
6 min readSep 30, 2019

--

Let’s get started! To begin, let’s make sure you have some dependencies on your OS. We will need:

I am going to hand wave over how to get all those set up, but please make sure those are all working prior to proceeding. The full code is available at https://github.com/cecotw/serverless-graphql-go. Medium has annoying formatting for code pasting, or I am just new to it, so please use that if you need more guidance.

Alright, let’s start at the very bottom of the stack here. Start up Postgres, create a database, and let’s run some migrations so its rearing to go with our API. A great idea (and exercise for the reader) is to setup a cloudformation template to deploy a database in the cloud. Aurora serverless or DynamoDb is a great fit for this model.

Create a project folder and initialize a git repo. Then, spawn a db and create some migrations.

CREATE DATABASE serverlessgraphqlgo --in a SQL shell
migrate create -seq -ext sql -dir db/migrations install_pgcrypto
migrate create -seq -ext sql -dir db/migrations create_todos

Now we should have a few database migrations files. Lets add an up to the pgycrytpo:

CREATE EXTENSION "pgcrypto"

and a down:

DROP EXTENSION IF EXISTS "pgcrypto"

And for the todos table up:

CREATE TABLE IF NOT EXISTS todos(
id UUID PRIMARY KEY,
message VARCHAR (200) NOT NULL,
is_complete BOOLEAN NOT NULL
);

and down:

DROP TABLE IF EXISTS todos;

Lets run them:

migrate -path db/migrations -database postgres://postgres@localhost:5432/serverlessgraphqlgo?sslmode=disable up

Feel free to add that up command and an analogous down to a makefile. Cool, now we should have a serverless-graphql-go database with a todos table consisting of a id column, message, and is_complete column. Nothing too exciting here, just laying the foundation. Ok, now for the fun.

Start by initializing a go module.

go mod init github.com/<profile>/<repo-name>

Now, let’s create our graphql microservice at ./cmd/graphql/main.go. For now, lets just add a main func and log a hello world

package graphqlimport "fmt"func main() {
fmt.Println("Hello World")
}

Word, now we are cooking. Lets add our handler function. Create it at ./internal/pkg/handler/graphql.go.

package handlerimport (
"context"
"encoding/json"
"errors"
"log"
"github.com/aws/aws-lambda-go/events"
"github.com/graph-gophers/graphql-go"
)
// GraphQl graphql handler
type GraphQl struct {
Schema *graphql.Schema
}
// BuildSchema builds schema
func (g *GraphQl) BuildSchema(schema string, resolver interface{}) {
opts := []graphql.SchemaOpt{graphql.UseFieldResolvers()}
g.Schema = graphql.MustParseSchema(schema, resolver, opts...)
}
var (
// ErrQueryNameNotProvided is thrown when a name is not provided
ErrQueryNameNotProvided = errors.New("no query was provided in the HTTP body")
)
// Lambda is the Lambda function handler
func (g *GraphQl) Lambda(context context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
log.Printf("Processing Lambda request %s\n", request.RequestContext.RequestID)
// If no query is provided in the HTTP request body, throw an error
if len(request.Body) < 1 {
return events.APIGatewayProxyResponse{}, ErrQueryNameNotProvided
}
var params struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]interface{} `json:"variables"`
}
if err := json.Unmarshal([]byte(request.Body), &params); err != nil {
log.Print("Could not decode body", err)
}
response := g.Schema.Exec(context, params.Query, params.OperationName, params.Variables)
responseJSON, err := json.Marshal(response)
if err != nil {
log.Print("Could not decode body")
}
return events.APIGatewayProxyResponse{
Body: string(responseJSON),
StatusCode: 200,
Headers: map[string]string{
"Access-Control-Allow-Origin": "*", // TODO use env var
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
"Access-Control-Allow-Headers": "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization",
},
}, nil
}

You may have to run a go get command to install the dependencies if your IDE isn’t set up to install them automatically. I recommend VSCode with the Go extension. Now lets update our main file to use the handler:

var graphql = new(handler.GraphQl) func main() {
lambda.Start(graphql.Lambda)
}

Update your imports here as well. So now we need to put on our devops hat, and utilize some of those utilities we installed earlier. touch template.yaml will stub out our AWS SAM CLI template. Populate it with:

---
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31Resources:
LocalApi:
Type: AWS::Serverless::Api
Properties:
StageName: local
Cors:
AllowHeaders: "'Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization'"
AllowOrigin: "'*'"
AllowMethods: "'POST, GET, OPTIONS'"
GraphqlFunction:
Type: AWS::Serverless::Function
Properties:
Handler: ./cmd/graphql/main
Runtime: go1.x
Events:
GetEvent:
Type: Api
Properties:
RestApiId: !Ref LocalApi
Path: /query
Method: post
Environment:
Variables:
DATABASE_USERNAME: postgres
DATABASE_PASSWORD: postgres
DATABASE_HOST: host.docker.internal
DATABASE_PORT: 5432
DATABASE: serverlessgraphqlgo
SSL_MODE: disable

If you haven’t already created a Makefile, please do so now. Please note, since our serverless function and fake API gateway will be deployed using the SAM CLI to a docker container that mirrors an AWS account, we will need to build our binary targeting linux architecture.

OUTPUT = ./cmd/graphql/main
.PHONY: clean
clean:
rm -f $(OUTPUT)
.PHONY: install
install:
go get ./...
graphql: ./cmd/graphql/main.go
go build -o $(OUTPUT) ./cmd/graphql/main.go
# compile the code to run in Lambda (local or real)
.PHONY: lambda
lambda:
GOOS=linux GOARCH=amd64 $(MAKE) graphql
.PHONY: build
build: clean lambda
.PHONY: local
local: build
sam local start-api
.PHONY: migrate-up
migrate-up:
migrate -path db/migrations -database postgres://postgres@localhost:5432/strutdb?sslmode=disable up
.PHONY: migrate-down
migrate-down:
migrate -path db/migrations -database postgres://postgres@localhost:5432/strutdb?sslmode=disable down

make local will compile our binary, and start the sam local utility. That uses the settings defined in the template.yaml to set up serverless functions. There we defined a mock API gateway, our handler and mappings, and some DB env variables so we can connect with the database we setup in the first section. You can start it up, but it probably won’t work correctly as we haven’t defined our schema or resolvers yet. Let’s get those cooking in the next section.

Ok, so, start by creating our schema file at ./internal/app/graphql/schema/schema.go. This file will use composition to include all the resolvers that we create. Populate and ignore the missing refs, we will create the todo.go file in a minute:

import "github.com/<user>/<reponame>/internal/app/graphql/todo"// QueryResolver : Query Resolver
type QueryResolver struct {
*todo.Resolver
}
var Schema = `schema {
query: Query
}
type Query {}
type Mutation {}
extend type Query {
todo(id: ID!): Todo
todos: [Todo]
}
extend type Mutation {
createTodo(input: TodoInput!): Todo!
}
type Todo {
id: ID!
message: String
isComplete: Boolean
}
input TodoInput {
id: ID
message: String
isComplete: Boolean!
}
`

Our ./internal/pkg/graphql/todo/todo.go file looks like this:

package todoimport (
"fmt"
"github.com/<user>/<repo>/internal/pkg/db"
"github.com/graph-gophers/graphql-go"
)
// Todo Todo
type Todo struct {
ID graphql.ID
Message *string
IsComplete *bool `db:"is_complete"`
}
// Input Todo Input
type Input struct {
ID *graphql.ID
Message string
IsComplete bool `db:"is_complete"`
}
// Resolver Todo Resolver
type Resolver struct{}
// Todos : Resolver function for the "Todo" query
func (r *Resolver) Todos() *[]*Todo {
db := db.Connect()
defer db.Close()
todos := []*Todo{}
db.Select(&todos, "SELECT * FROM todos")
return &todos
}
// Todo : Resolver function for the "Todo" query
func (r *Resolver) Todo(args struct{ ID graphql.ID }) *Todo {
db := db.Connect()
defer db.Close()
todo := &Todo{}
db.Get(&todo, "SELECT * FROM todos WHERE id=$1", args.ID)
return todo
}
// CreateTodo : Create a todo
func (r *Resolver) CreateTodo(args struct{ Input Input }) *Todo {
db := db.Connect()
defer db.Close()
row := db.QueryRowx(
"INSERT INTO todos (id, message, is_complete) VALUES (gen_random_uuid(), $1, $2) RETURNING *",
&args.Input.Message,
&args.Input.IsComplete,
)
todo := &Todo{}
err := row.StructScan(todo)
if err != nil {
fmt.Println(err.Error())
}
return todo
}

This references a db file to access the database at ./internal/pkg/db/db.go:

package dbimport (
"fmt"
"log"
"os"
"github.com/jmoiron/sqlx"
// Postgres Driver
_ "github.com/lib/pq"
)
// Connect connect to a database
func Connect() *sqlx.DB {
dbinfo := fmt.Sprintf(
"user=%s password=%s host=%s port=%s dbname=%s sslmode=%s",
os.Getenv("DATABASE_USERNAME"),
os.Getenv("DATABASE_PASSWORD"),
os.Getenv("DATABASE_HOST"),
os.Getenv("DATABASE_PORT"),
os.Getenv("DATABASE"),
os.Getenv("SSL_MODE"),
)
db, err := sqlx.Open("postgres", dbinfo)
if err != nil {
log.Fatal(err.Error())
}
return db
}

All this does is consume our db env vars and form a connection to the database. The resolver is where the real magic happens. This utilizes graphql-go’s library to resolve the queries with database values. Only thing left is to tie it all together.

Start by adding an init method to our main.go:

func init() {
graphql.BuildSchema(schema.Schema, &schema.QueryResolver{})
}

That should be it! You will need to run make install and rebuild your microservice with make build. That should be a simple graphql server running as a serverless function!

In the next part, we will setup a UI using Vue.js and Apollo to display our todo list to end users. Part three will focus on deployment and Infrastructure as Code (Cloudformation) to get the entire thing up and running in the cloud as a modern, scalable web application.

--

--