DEV Community

Christian Melgarejo
Christian Melgarejo

Posted on • Edited on

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

Top comments (8)

Collapse
 
mike1808 profile image
Michael Manukyan • Edited

Hi, I appreciate the effort of creating this, but it contains quite many errors and if some beginner tries to follow these steps she will be blocked by them.

  1. In the step 2) the pathGQL variable is not defined
  2. You're creating file pkg/srv/main.go but then referring to pkg/server
  3. In the last step you should refer to cmd/server/main.go but you referre to cmd/gql-server/main.go.
Collapse
 
cmelgarejo profile image
Christian Melgarejo • Edited

Hi Michael, thanks for replying!

On points 1 and 2, you're right, good catches, I've fixed that missing variable and typo now.

I saw what you meant, on point 3, pointing to the right file right now.

Let me tell you that I've marked the post for advanced programmers for that reason, we all can err != nil from time to time and that's why I'm sharing, and that helps all us improve.

Collapse
 
mike1808 profile image
Michael Manukyan

So what was the point of changing that file to that one-liner if you then modifying it to the initial code with small changes?

Thread Thread
 
cmelgarejo profile image
Christian Melgarejo • Edited

Sorry, which one liner?
EDIT: Now should be pointing to the right file, everything should make sense now.

Thread Thread
 
mike1808 profile image
Michael Manukyan

So, we created this file cmd/gql-server/main.go, then we are moving that files logic to pkg/server/main.go and calling the exposed Run() function. Afterwards we add this utility which helps to read from .env file. However, we then are using these utility functions back in cmd/gql-server/main.go where we have Server.run(). I suppose we need to modify the file in pkg folder.

Am I missing something? I'm just trying to follow the guide.

Sorry if my initial comment sounded rude, didn't intend to that.

Thread Thread
 
cmelgarejo profile image
Christian Melgarejo

Yes you were correct, I've modified the source according your observations, thank you Michael!

Collapse
 
xuanlong5 profile image
xuanlong • Edited

Too complex with row: unset $(grep -v '^#' .env | sed -E 's/(.)=./\1/' | xargs).

It takes many my times to research bash shell script

Collapse
 
cmelgarejo profile image
Christian Melgarejo

Actually this line just takes the .env file (that you should copy from .env.example, and modify accordingly) and uses sed and xargs to line by line (without comments) un-export the environment variables, just a convenience script, no need to use it if you don't want to :)