DEV Community

Cover image for Learning Go by examples: part 2 - Create an HTTP REST API Server in Go
Aurélie Vache
Aurélie Vache

Posted on • Updated on

Learning Go by examples: part 2 - Create an HTTP REST API Server in Go

In the first article we setted up our environment. Now, we will create our first application: an HTTP REST API Server in Go.

Initialization

First of all, we can create our repository in GitHub (in order to share and open-source it).

For that, I logged in GitHub website, clicked on the repositories link, click on "New" green button and then I created a new repository called “learning-go-by-example”.

Now, in your local computer, git clone this new repository where you want:

$ git clone https://github.com/scraly/learning-go-by-examples.git
$ cd learning-go-by-examples
Enter fullscreen mode Exit fullscreen mode

As we will re-used this Git repository, we will create a folder go-rest-api for our first application and go into it:

$ mkdir go-rest-api
$ cd go-rest-api
Enter fullscreen mode Exit fullscreen mode

Now, we have to initialize Go modules (dependency management):

$ go mod init github.com/scraly/learning-go-by-examples/go-rest-api
go: creating new go.mod: module github.com/scraly/learning-go-by-examples/go-rest-api
Enter fullscreen mode Exit fullscreen mode

This will create a go.mod file like this:

module github.com/scraly/learning-go-by-examples/go-rest-api

go 1.16
Enter fullscreen mode Exit fullscreen mode

Before to start our awesome API, as good practices, we will create a simple code organization.

Create the following folders organization:

.
├── README.md
├── bin
├── doc
├── go.mod
├── internal
├── pkg
    └── swagger
Enter fullscreen mode Exit fullscreen mode

Let's create our HTTP server

Go is a powerful language and it comes with a huge number of useful libraries in its ecosystem, like net/http that will interest us.

We will start to create a main.go file in the internal/ folder:

package main

import (
    "fmt"
    "html"
    "log"
    "net/http"
) 

func main() {

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
    })

    log.Println("Listening on localhost:8080")

    log.Fatal(http.ListenAndServe(":8080", nil))
}
Enter fullscreen mode Exit fullscreen mode

This simple example starts an HTTP server, listens on port 8080 incoming requests, serves on / and return "Hello" + the path.

Now, it’s time to run our app in order to test it:

$ go run internal/main.go

2021/07/12 22:10:47 Listening on localhost:8080
Enter fullscreen mode Exit fullscreen mode

In order to test our HTTP server, we can curl on localhost:8080 or go to this endpoint in your browser:

$ curl localhost:8080

Hello, "/"%
Enter fullscreen mode Exit fullscreen mode

So we just need to write some Go code in a .go file and then run go run myfile.go in order to test it, awesome!

Yes, it's great, but if we want, we can also generate an executable binary with our HTTP server:

$ go build -o bin/go-rest-api internal/main.go
Enter fullscreen mode Exit fullscreen mode

Makefile -> Taskfile

It's cool to execute all of our commands in order to run our app, package it, test it, generate swagger doc... but what do you think if we can automate it?

In the past I used Makefile in order to define a set of tasks but now I used Taskfile, a Makefile alternative.

I recommend you the article by Sébastien Kurtzemann about Taskfile who explain what is Taskfile, how to install it and how to create your first tasks.

For our app, I created a Taskfile.yml file with this content:

version: "3"

tasks:
    build:
        desc: Build the app
        cmds:
        - GOFLAGS=-mod=mod go build -o bin/go-rest-api internal/main.go 

    run: 
        desc: Run the app
        cmds:
        - GOFLAGS=-mod=mod go run internal/main.go

    swagger.gen:
        desc: Generate Go code
        cmds:
        - GOFLAGS=-mod=mod go generate github.com/scraly/learning-go-by-examples/go-rest-api/internal github.com/scraly/learning-go-by-examples/go-rest-api/pkg/swagger

    swagger.validate:
        desc: Validate swagger
        cmds:
        - swagger validate pkg/swagger/swagger.yml

    swagger.doc:
        desc: Doc for swagger
        cmds:
        - docker run -i yousan/swagger-yaml-to-html < pkg/swagger/swagger.yml > doc/index.html
Enter fullscreen mode Exit fullscreen mode

If you want, you can download its latest version in the GitHub repository.

Install task in your local machine, in order to do that you can follow installation instructions or if you have a MacOS with homebrew, you can use brew install command:

$ brew install go-task/tap/go-task
Enter fullscreen mode Exit fullscreen mode

Now you can display the list of available tasks:

$ task --list
task: Available tasks for this project:
* build:        Build the app
* run:          Run the app
* swagger.doc:      Doc for swagger
* swagger.gen:      generate Go code
* swagger.validate:     Validate swagger
Enter fullscreen mode Exit fullscreen mode

Cool, we learned another good practice!

It's time to create our REST API

Cool, we code an HTTP server, but Aurélie you talked about a REST API, isn't it?

It's true, we will now step up our HTTP server and use Swagger, which handles definitions of our HTTP endpoints.

What is Swagger?

Swagger

Swagger allows you to provide standardized documentation of your APIs compliant to OpenAPI specifications.

With a swagger specification file in input, thanks to the Swagger application, you can generate the code and at the end, and you can provide users the API documentation in HTML.

If you want to build a public API, don't hesitate to use Swagger.

Swagger installation

We will install go-swagger tool, don't hesitate to follow installation page.

If you have a Mac:

$ brew tap go-swagger/go-swagger
$ brew install go-swagger
Enter fullscreen mode Exit fullscreen mode

And then, you can check the version of Swagger app in order to verify the tool is correctly installed in your system:

$ swagger version
version: v0.27.0
commit: 43c2774170504d87b104e3e4d68626aac2cd447d
Enter fullscreen mode Exit fullscreen mode

Let's create our swagger specification in a new file called pkg/swagger/swagger.yml:

consumes:
- application/json
info:
  description: HTTP server in Go with Swagger endpoints definition.
  title: go-rest-api
  version: 0.1.0
produces:
- application/json
schemes:
- http
swagger: "2.0"

paths:
  /healthz:
    get:
      operationId: checkHealth
      produces:
      - text/plain
      responses:
        '200':
          description: OK message.
          schema:
            type: string
            enum:
            - OK

  /hello/{user}:
    get:
      description: Returns a greeting to the user!
      parameters:
        - name: user
          in: path
          type: string
          required: true
          description: The name of the user to greet.
      responses:
        200:
          description: Returns the greeting.
          schema:
            type: string
        400:
          description: Invalid characters in "user" were provided.

  /gopher/{name}:
    get:
      description: Return the Gopher Image.
      produces:
      - image/png
      parameters:
        - name: name
          in: path
          type: string
          required: true
          description: The name of the Gopher to display.
      responses:
        200:
          description: Returns the Gopher.
          schema:
            type: file
Enter fullscreen mode Exit fullscreen mode

After each modification of a swagger file, a good practice is to check the validity of the file:

$ task swagger.validate
task: [swagger.validate] swagger validate pkg/swagger/swagger.yml
2021/07/12 22:39:47
The swagger spec at "pkg/swagger/swagger.yml" is valid against swagger specification 2.0
Enter fullscreen mode Exit fullscreen mode

Cool, our swagger file is valid.

We will now create our swagger definitions in an HTML doc.
For that, I use a docker image, which takes into consideration our swagger YAML definition and returns a pretty HTML page:

$ task swagger.doc
task: [swagger.doc] docker run -i yousan/swagger-yaml-to-html < pkg/swagger/swagger.yml > doc/index.html
Enter fullscreen mode Exit fullscreen mode

If you open the generated doc/index.html page in a browser, you can view HTML endpoints definitions:
Swagger UI

The Swagger doc is human readable and perfect when you create and distribute an API.

Now we can generate Go code thanks to swagger specifications.

In order to do this, go in the package pkg/swagger/ and now create a gen.go file with this content:

package swagger

//go:generate rm -rf server
//go:generate mkdir -p server
//go:generate swagger generate server --quiet --target server --name hello-api --spec swagger.yml --exclude-main
Enter fullscreen mode Exit fullscreen mode

Let's generate Go files accoring to Swagger specifications:

$ task swagger.gen
task: [swagger.gen] GOFLAGS=-mod=mod go generate github.com/scraly/learning-go-by-examples/go-rest-api/internal github.com/scraly/learning-go-by-examples/go-rest-api/pkg/swagger
Enter fullscreen mode Exit fullscreen mode

The command will generate several useful files (containing handlers, struct, functions...):

pkg/swagger
├── gen.go
├── server
│   └── restapi
│       ├── configure_hello_api.go
│       ├── doc.go
│       ├── embedded_spec.go
│       ├── operations
│       │   ├── check_health.go
│       │   ├── check_health_parameters.go
│       │   ├── check_health_responses.go
│       │   ├── check_health_urlbuilder.go
│       │   ├── get_gopher_name.go
│       │   ├── get_gopher_name_parameters.go
│       │   ├── get_gopher_name_responses.go
│       │   ├── get_gopher_name_urlbuilder.go
│       │   ├── get_hello_user.go
│       │   ├── get_hello_user_parameters.go
│       │   ├── get_hello_user_responses.go
│       │   ├── get_hello_user_urlbuilder.go
│       │   └── hello_api_api.go
│       └── server.go
└── swagger.yml
Enter fullscreen mode Exit fullscreen mode

It’s time-saving for our HTTP REST API server implementation.

Let's implement our routes!

Back to the future Gopher

Let’s edit our main.go file with this new content:

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/go-openapi/loads"
    "github.com/go-openapi/runtime/middleware"
    "github.com/scraly/learning-go-by-examples/go-rest-api/pkg/swagger/server/restapi"

    "github.com/scraly/learning-go-by-examples/go-rest-api/pkg/swagger/server/restapi/operations"
)

func main() {

    // Initialize Swagger
    swaggerSpec, err := loads.Analyzed(restapi.SwaggerJSON, "")
    if err != nil {
        log.Fatalln(err)
    }

    api := operations.NewHelloAPIAPI(swaggerSpec)
    server := restapi.NewServer(api)

    defer func() {
        if err := server.Shutdown(); err != nil {
            // error handle
            log.Fatalln(err)
        }
    }()

    server.Port = 8080

    api.CheckHealthHandler = operations.CheckHealthHandlerFunc(Health)

    api.GetHelloUserHandler = operations.GetHelloUserHandlerFunc(GetHelloUser)

    api.GetGopherNameHandler = operations.GetGopherNameHandlerFunc(GetGopherByName)

    // Start server which listening
    if err := server.Serve(); err != nil {
        log.Fatalln(err)
    }
}

//Health route returns OK
func Health(operations.CheckHealthParams) middleware.Responder {
    return operations.NewCheckHealthOK().WithPayload("OK")
}

//GetHelloUser returns Hello + your name
func GetHelloUser(user operations.GetHelloUserParams) middleware.Responder {
    return operations.NewGetHelloUserOK().WithPayload("Hello " + user.User + "!")
}

//GetGopherByName returns a gopher in png
func GetGopherByName(gopher operations.GetGopherNameParams) middleware.Responder {

    var URL string
    if gopher.Name != "" {
        URL = "https://github.com/scraly/gophers/raw/main/" + gopher.Name + ".png"
    } else {
        //by default we return dr who gopher
        URL = "https://github.com/scraly/gophers/raw/main/dr-who.png"
    }

    response, err := http.Get(URL)
    if err != nil {
        fmt.Println("error")
    }

    return operations.NewGetGopherNameOK().WithPayload(response.Body)
}

Enter fullscreen mode Exit fullscreen mode

We use several Go librairies, so you can execute go get <my-lib> command with the dependancies you need or copy/paste the require dependencies block in your go.mod file:

module github.com/scraly/learning-go-by-examples/go-rest-api

go 1.16

require (
    github.com/go-openapi/errors v0.20.0
    github.com/go-openapi/loads v0.20.2
    github.com/go-openapi/runtime v0.19.29
    github.com/go-openapi/spec v0.20.1
    github.com/go-openapi/strfmt v0.20.0
    github.com/go-openapi/swag v0.19.13
    github.com/jessevdk/go-flags v1.5.0
    golang.org/x/net v0.0.0-20210119194325-5f4716e94777
)
Enter fullscreen mode Exit fullscreen mode

As you can see, in our main.go file, we initialize a REST API swagger server and we define 3 handlers (and their implementation:

  • CheckHealthHandler
  • GetHelloUserHandler
  • GetGopherNameHandler

Let's dig into these 3 handlers implementation:

Health route -> /healthz

According to the following code:

//Health route returns OK
func Health(operations.CheckHealthParams) middleware.Responder {
    return operations.NewCheckHealthOK().WithPayload("OK")
}
Enter fullscreen mode Exit fullscreen mode

When a user calls /healthz route, we'll send them a response with OK as string.

HelloUser route -> /hello/{user}

According to the following code:

//GetHelloUser returns Hello + user name
func GetHelloUser(user operations.GetHelloUserParams) middleware.Responder {
    return operations.NewGetHelloUserOK().WithPayload("Hello " + user.User + "!")
}
Enter fullscreen mode Exit fullscreen mode

When a user calls /hello/{user} route, we'll send them a response with "Hello" + {user} as string.

GopherName route -> /gopher/{name}

According to the following code:

//GetGopherByName returns a gopher in png
func GetGopherByName(gopher operations.GetGopherNameParams) middleware.Responder {

    var URL string
    if gopher.Name != "" {
        URL = "https://github.com/scraly/gophers/raw/main/" + gopher.Name + ".png"
    } else {
        //by default we return dr who gopher
        URL = "https://github.com/scraly/gophers/raw/main/dr-who.png"
    }

    response, err := http.Get(URL)
    if err != nil {
        fmt.Println("error")
    }

    return operations.NewGetGopherNameOK().WithPayload(response.Body)
}
Enter fullscreen mode Exit fullscreen mode

When a user calls /gopher/{name} route, they get a cute gopher image and then send the image back to the user. If name is empty, we will return our Doctor Who gopher by default.

Let's build our app...

In Go you can easily build an app in an executable binary file:

$ go build -o bin/go-rest-api internal/main.go
Enter fullscreen mode Exit fullscreen mode

The command will generate the executable binary in bin/ folder.

Or you can execute the run task:

$ task build
task: [build] GOFLAGS=-mod=mod go build -o bin/go-rest-api internal/main.go
Enter fullscreen mode Exit fullscreen mode

Now we can execute our binary file:

$ ./bin/go-rest-api
2021/07/13 20:21:34 Serving hello API at http://[::]:8080
Enter fullscreen mode Exit fullscreen mode

Cool!

...for others environments/OS

If you want to go more deeper, what I like in Go is that you can generate your app (an executable binary) for multiple environments, not only yours! In other words, you can cross compile Go apps for macOS, Windows, Linux... with go build command.

Cross compile Gopher

For Windows:

# Windows 32 bits
$ GOOS=windows GOARCH=386 go build -o bin/go-rest-api-win-386 internal/main.go

# Windows 64 bits
$ GOOS=windows GOARCH=amd64 go build -o bin/go-rest-api-win-64 internal/main.go
Enter fullscreen mode Exit fullscreen mode

For Linux:

# Linux 32 bits
$ GOOS=linux GOARCH=386 go build -o bin/go-rest-api-linux-386 internal/main.go

# Linux 64 bits
$ GOOS=linux GOARCH=amd64 go build -o bin/go-rest-api-linux-64 internal/main.go
Enter fullscreen mode Exit fullscreen mode

And for MacOS:

# MacOS 32 bits
$ GOOS=darwin GOARCH=386 go build -o bin/go-rest-api-darwin-386 internal/main.go

# MacOS 64 bits
$ GOOS=darwin GOARCH=amd64 go build -o bin/go-rest-api-darwin-64 internal/main.go

# MacOS 64 bits for M1 chip
$ GOOS=darwin GOARCH=arm64 go build -o bin/go-rest-api-darwin-arm64 internal/main.go
Enter fullscreen mode Exit fullscreen mode

Now you can share your awesome app to your friends :-).

Let's test our app

Our app is running, so now we can test our routes with curl command:

$ curl localhost:8080
{"code":404,"message":"path / was not found"}%
Enter fullscreen mode Exit fullscreen mode

This path is not defined, we have a 404 error code, normal :-).

$ curl localhost:8080/healthz
OK

$ curl localhost:8080/hello/aurelie
"Hello aurelie!"

$ curl -O localhost:8080/gopher/dr-who
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  992k    0  992k    0     0   955k      0 --:--:--  0:00:01 --:--:--  955k

$ file dr-who
dr-who: PNG image data, 1700 x 1460, 8-bit/color RGBA, non-interlaced
Enter fullscreen mode Exit fullscreen mode

You can also go to localhost:8080/gopher/dr-who in your browser to display our little Gopher :-).

Doctor Who Gopher

Perfect! :-)

Conclusion

As we have seen in this article, it's possible to create a simple HTTP server in several seconds and an HTTP REST API server in Go in minutes.

All the code is available in: https://github.com/scraly/learning-go-by-examples/tree/main/go-rest-api

In the following articles we will create others kind/types of applications in Go.

Hope you'll like it.

Top comments (22)

Collapse
 
cadayton profile image
Craig Dayton

Correction the cloned instance of go-rest-api is working. It was my effort to duplicate this setup in a localized project folder where the issues come into play.

Collapse
 
aurelievache profile image
Aurélie Vache

Hi, so is it working finally?

Collapse
 
cadayton profile image
Craig Dayton

Yes the downloaded git version of go-rest-api is working. It is when I attempt to create a local copy that I run into issues. I'm struggling with what needs to be a module versus just a regular local package and getting the import references working correctly. I've yet to see any clear documentation regarding the use of modules versus just local packages and how to structure project correctly. So, I'm spinning my wheels in attempts to come to a self understanding regarding modules in the combination with imports. As a beginner, the last thing I want to do is to start pushing worthless code to GitHub. I'm on POP!_OS and using VScode. VScode seems to be getting in the way at times too. Anyway, thanks for making available various examples I've learned a lot from them. I'll continue my struggle with Golang, but I seriously do not understand why it has to be so complicated.

Collapse
 
streamdp profile image
Alexandr Primak • Edited

Thank you very much! This is a great course. love gopher

Collapse
 
haikal00 profile image
haikal00

Thank you so much!

Collapse
 
arroyoruy profile image
Dan Arroyo

Thank you very much. I learned a lot. My dev environment is Ubuntu and had problems installing Swagger. Able to use docker method to run it, but in Taskfile.yml I had to change 'cmds' to included the entire docker run command. Otherwise no other issues. I can't wait for next one. also Definitely blues-gopher is my favorite one.

Collapse
 
aurelievache profile image
Aurélie Vache

Thanks Dan for your feedback 🙂.
I had problem in the past with installing swagger in a VM With Ubuntu too (4-5 years ago).

Yes using sicker image is a good solution and with Taskfile you can add or edit tasks it's a good way 👍

Ohh you like the blues gophers 😊 I love them too, the color match very well with gophers.

I'll publish another article next Wednesday I think 🤞💪

Collapse
 
michellejae profile image
michelle

heads up, if your doing this on an apple m1 machine i believe you'll need to add "--platform linux/amd64" to the taskfile on the docker command. :)

also sorry aurélie i accidentally posted this comment to part 1.

Collapse
 
aurelievache profile image
Aurélie Vache

Thanks Michelle.
I didn't test on M1 for the moment, I'll do it and update the article .

Collapse
 
houcemabdellatif profile image
Houcem Abdellatif

Thank you, good content. I run into a problem when generating files. When running the cmd: "task swagger.gen". did not generate the files needed. I got the error msg: "go: not generating in packages in dependency modules". any help

Collapse
 
pdevgl profile image
Mr Poyla

In Taskfile, you could just replace "github.com/scraly/learning-go-by-examples/go-rest-api/internal" for your own module name.

Collapse
 
aiengineer13 profile image
AIEngineer13

im having the same problem im using the "task swagger.gen" i tried to swich de version of my language but im running to the same problem

Collapse
 
mielofon profile image
Alexey Ponomarev

Thank you very much.
I repeated this code following you. But I have a problem with URL localhost:8080
It return
{"code":404,"message":"path /gopher/ was not found"}
because 'required: true' in swagger.yml for /gopher/{name}:

Collapse
 
aurelievache profile image
Aurélie Vache

Hi, thanks, yes if you follow the "let's test our app" part, I test "/healthz" route, "/hello/aurelie" and "gopher/dr-who" route :-) .

Never "/gopher" alone.

The goal is to retrieve an existing gopher so you can test with a gopher name ;-).

Collapse
 
julianperezpesce profile image
Julián Pérez Pesce

Amazing!!! Thank you again Aurélie

Collapse
 
cadayton profile image
Craig Dayton

I've learned much trying to getting the tutorials to work, but all tutorials are failing to execute for one reason or another. This includes the tutorials cloned with git. The last tutorial was the go-rest-api and very much like the approach but not repeatable for me.
I'm just getting started with Golang and finding it very frustrating not being able to find functional tutorials. Anyway, when I'm more experienced I'll come back to these tutorials and see what my newbie issues were.

Collapse
 
cadayton profile image
Craig Dayton

localized copy of go-rest-api now working.

Issue 1: 'task gen' not generating server code in /pkg/swagger
change to /pkg/swagger and executing 'go generate gen.go' worked.

Issue 2: Had to manually set import statement in server.go to match the git version

Issue 3: go.mod had to be manually created to match the git version.

VScode is issuing a warning that 'io/ioutil' has been deprecated since Go 1.16

Collapse
 
aurelievache profile image
Aurélie Vache

💪
thanks for your remark about io/ioutil package:
pkg.go.dev/io/ioutil

Collapse
 
prondubuisi profile image
Onyemenam Ndubuisi

I am getting started with go and I have learnt a lot including best practices using your example. Kudos.

Collapse
 
aurelievache profile image
Aurélie Vache

Thanks for this kind comment ♥️

Collapse
 
codeanit profile image
Anit Shrestha Manandhar

I believe that you are a good technical leader and mentor Aurelie. To explain in an easy way is one of the most challenging part of being a good leader! Enjoying your post! Keep them coming! Cheers,

Collapse
 
aurelievache profile image
Aurélie Vache

Thanks Anit 🥰