Creating an opinionated GraphQL server with Go - Part 1

cmelgarejo

Christian Melgarejo

Posted on July 16, 2019

Creating an opinionated GraphQL server with Go - Part 1

Let's make an opinionated GraphQL server using:

This assumes you have:

  • At least, basic Go knowledge, and have go 1.12+ installed.
  • VSCode (preferred) or similar IDE

Project Setup

We'll follow the Go standard project layout, for this service, take a look at
the specification, it is opinionated, but serves as a good base, and we might
strand from it a little bit from it's guidelines.

Start by creating a directory anywhere we want to:

$ mkdir go-gql-server
$ cd go-gql-server
/go-gql-server $
Enter fullscreen mode Exit fullscreen mode

Let's create the whole project layout with:

$ mkdir -p {build,cmd/gql-server,internal/gql-server,pkg,scripts}
# directories are created
Enter fullscreen mode Exit fullscreen mode
  • internal/gql-server will hold all the related files for the gql server
  • cmd/gql-server will house the main.go file for the server, the entrypoint that will glue all together.

Since we're using go 1.12+ that will allow to use any directory you want outside
the $GOPATH/src path, we want to use go modules to initialize our project
with it we have to run:

$ go mod init github.com/cmelgarejo/go-gql-server # Replace w/your user handle
go: creating new go.mod: module github.com/cmelgarejo/go-gql-server
Enter fullscreen mode Exit fullscreen mode

Coding our web server

1. Web framework: Gin

So, now we can start adding packages to our project! Let's start by getting our
web framework: gin-gonic

From gin-gonic.com:

What is Gin? Gin is a web framework written in Golang. It features a
martini-like API with much better performance, up to 40 times faster. If you
need performance and good productivity, you will love Gin.

$ go get -u github.com/gin-gonic/gin
go: creating new go.mod: module cmd/gql-server/main.go
Enter fullscreen mode Exit fullscreen mode

2. Code the web server

So lets start creating the web server, to keep going, create a main.go file
in cmd/gql-server

$ vi cmd/gql-server/main.go
# vi ensues, I hope you know how to exit
Enter fullscreen mode Exit fullscreen mode

And paste this placeholder code:

package main

import (
    "log"
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    host := "localhost"
    port := "7777"
    pathGQL := "/graphql"
    r := gin.Default()
    // Setup a route
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, "OK")
    })
    // Inform the user where the server is listening
    log.Println("Running @ http://" + host + ":" + port + pathGQL)
    // Print out and exit(1) to the OS if the server cannot run
    log.Fatalln(r.Run(host + ":" + port))
}
Enter fullscreen mode Exit fullscreen mode

If you go run cmd/gql-server/main.go this code, it will bring up a GIN server
listening to locahost:7777 and get an OK printed
out in the browser. It works! Now, lets refactor this code using our already
present directory structure, script, internal and pkg folders

  • internal/handlers/ping.go
package handlers

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

// Ping is simple keep-alive/ping handler
func Ping() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.String(http.StatusOK, "OK")
    }
}
Enter fullscreen mode Exit fullscreen mode
  • pkg/server/main.go
package server

import (
    "log"

    "github.com/gin-gonic/gin"
    "github.com/cmelgarejo/go-gql-server/internal/handlers"
)

var HOST, PORT string

func init() {
    HOST = "localhost"
    PORT = "7777"
}

// Run web server
func Run() {
    r := gin.Default()
    // Setup routes
    r.GET("/ping", handlers.Ping())
    log.Println("Running @ http://" + HOST + ":" + PORT )
    log.Fatalln(r.Run(HOST + ":" + PORT))
}
Enter fullscreen mode Exit fullscreen mode
  • cmd/gql-server/main.go file can be changed to just this:
package main

import (
    "github.com/cmelgarejo/go-gql-server/pkg/server"
)

func main() {
    server.Run()
}

Enter fullscreen mode Exit fullscreen mode

How about it? One line and we have our server running out of the pkg folder

Now we can build up the server with a script:

  • scripts/build.sh (/bin/sh because it is more obiquitus)
#!/bin/sh
srcPath="cmd"
pkgFile="main.go"
outputPath="build"
app="gql-server"
output="$outputPath/$app"
src="$srcPath/$app/$pkgFile"

printf "\nBuilding: $app\n"
time go build -o $output $src
printf "\nBuilt: $app size:"
ls -lah $output | awk '{print $5}'
printf "\nDone building: $app\n\n"
Enter fullscreen mode Exit fullscreen mode

And before running, make sure you chmod +x it

$ chmod +x scripts/build.sh
# sets execution permission on file
Enter fullscreen mode Exit fullscreen mode

Now we can start building our server, like so:

$ .scripts/build.sh

Building: gql-server

real    0m0.317s
user    0m0.531s
sys     0m0.529s

Built: gql-server size:16M

Done building: gql-server
Enter fullscreen mode Exit fullscreen mode

16M standalone server, not bad I think, and this could be the size of it's
docker image! Ok, now onto trying out what has been built:

$ ./build/gql-server
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> github.com/cmelgarejo/go-gql-server/internal/handlers.Ping.func1 (3 handlers)
2019/07/13 00:00:00 Running @ http://localhost:7777/gql
[GIN-debug] Listening and serving HTTP on :7777
[GIN] 2019/07/13 - 00:00:00 | 200 |      38.936µs |             ::1 | GET      /ping
Enter fullscreen mode Exit fullscreen mode

That's some serious speed, 39 µ(micro) seconds.

And we can further improve our server code, and make configurations load from a
.env file, let's create some utils for our server:

  • pkg/utils/env.go
package utils

import (
    "log"
    "os"
    "strconv"
)

// MustGet will return the env or panic if it is not present
func MustGet(k string) string {
    v := os.Getenv(k)
    if v == "" {
        log.Panicln("ENV missing, key: " + k)
    }
    return v
}

// MustGetBool will return the env as boolean or panic if it is not present
func MustGetBool(k string) bool {
    v := os.Getenv(k)
    if v == "" {
        log.Panicln("ENV missing, key: " + k)
    }
    b, err := strconv.ParseBool(v)
    if err != nil {
        log.Panicln("ENV err: [" + k + "]\n" + err.Error())
    }
    return b
}
Enter fullscreen mode Exit fullscreen mode

The code is pretty much self-explainatory, if a ENV var does not exists, the
program will panic, we need these to run. Now, changing:

  • pkg/server/main.go to this:
package server

import (
    "log"

    "github.com/gin-gonic/gin"
    "github.com/cmelgarejo/go-gql-server/internal/handlers"
    "github.com/cmelgarejo/go-gql-server/pkg/utils"
)

var host, port string

func init() {
    host = utils.MustGet("GQL_SERVER_HOST")
    port = utils.MustGet("GQL_SERVER_PORT")
}

// Run spins up the server
func Run() {
    r := gin.Default()
    // Simple keep-alive/ping handler
    r.GET("/ping", handlers.Ping())
    // Inform the user where the server is listening
    log.Println("Running @ http://" + host + ":" + port)
    // Print out and exit(1) to the OS if the server cannot run
    log.Fatalln(r.Run(host + ":" + port))

}
Enter fullscreen mode Exit fullscreen mode

And we see how's it is starting to take form as a well laid out project!

We can still make a couple of things, like running server locally by using this
script:

  • scripts/run.sh (don't forget to chmod +x this one too)
#!/bin/sh
srcPath="cmd"
pkgFile="main.go"
app="gql-server"
src="$srcPath/$app/$pkgFile"

printf "\nStart running: $app\n"
# Set all ENV vars for the server to run
export $(grep -v '^#' .env | xargs) && time go run $src
# This should unset all the ENV vars, just in case.
unset $(grep -v '^#' .env | sed -E 's/(.*)=.*/\1/' | xargs)
printf "\nStopped running: $app\n\n"
Enter fullscreen mode Exit fullscreen mode

This will set up ENV vars, and go run the server.

You can also add a .gitignore file, run git init set up the origin to your
own repository and git push it :)

This will continue in Part 2 where we'll add the GQLGen portion of
the server!

All the code is available here

💖 💪 🙅 🚩
cmelgarejo
Christian Melgarejo

Posted on July 16, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related