Let's make an opinionated GraphQL server using:
- Gin-gonic web framework
- Goth for OAuth2 connections
- GORM as DB ORM
- GQLGen for building GraphQL servers without any fuss
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 $
Let's create the whole project layout with:
$ mkdir -p {build,cmd/gql-server,internal/gql-server,pkg,scripts}
# directories are created
-
internal/gql-server
will hold all the related files for the gql server -
cmd/gql-server
will house themain.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
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
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
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))
}
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")
}
}
-
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))
}
-
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()
}
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"
And before running, make sure you chmod +x
it
$ chmod +x scripts/build.sh
# sets execution permission on file
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
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
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
}
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))
}
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 tochmod +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"
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)
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.
pathGQL
variable is not definedpkg/srv/main.go
but then referring topkg/server
cmd/server/main.go
but you referre tocmd/gql-server/main.go
.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.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?
Sorry, which one liner?
EDIT: Now should be pointing to the right file, everything should make sense now.
So, we created this file
cmd/gql-server/main.go
, then we are moving that files logic topkg/server/main.go
and calling the exposedRun()
function. Afterwards we add this utility which helps to read from.env
file. However, we then are using these utility functions back incmd/gql-server/main.go
where we haveServer.run()
. I suppose we need to modify the file inpkg
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.
Yes you were correct, I've modified the source according your observations, thank you Michael!
Too complex with row: unset $(grep -v '^#' .env | sed -E 's/(.)=./\1/' | xargs).
It takes many my times to research bash shell script
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 :)