Intro

In this post we will go through the process of building a simple Rest API go application.

Initially we will create a callback app in Go which will have one Rest API endpoint, then in the next part we will extend the application with CRUD DB Operations. That is, we are going to give our application a state and implement some form of authentication.

Create a new Project

First let us create a working directory named callback which from now on will call rootDir


mkdir -vp callback
cd callback

We should initiate go mod in order to handle any dependencies in our code automatically. To initiate go mode run the following code by changing ulfox with your username.


go mod init github.com/ulfox/callback

The rootDir should now contain a file named go.mod


ls . 
go.mod

Router

Before we create our main let us start by creating the router which will be the place that defines the logic for our Endpoint Handlers

We start by creating a new directory called router


mkdir -p router

Next create a file called router.go in router directory with the code below

package router

import (
	"errors"

	"github.com/gorilla/mux"
	"github.com/sirupsen/logrus"
)

// Service for creating a new router with logging
type Service struct {
	Router *mux.Router
	Name   string
	logger logrus.FieldLogger
}

// UpdateRoutes method updates main route with our Handle Functions
func (s *Service) UpdateRoutes() {
	s.Router.HandleFunc("/api/v1/ping", s.Callback).Methods("GET")
}

// NewRouter factory for creating a Service router
func NewRouter(name string, logger logrus.FieldLogger) (*Service, error) {
	if name == "" {
		return nil, errors.New("Name not allowed to be empty")
	}

	return &Service{
		Router: mux.NewRouter().StrictSlash(true),
		Name:   name,
		logger: logger,
	}, nil
}

In the code above we defined:

  • Lines (2 - 8): The import block with the required modules.
  • Lines (11 - 15): A Service for our router
  • Lines (18 - 20): UpdateRoute Method that updates the main service route with the endpoint handle functions
  • Lines (23 - 33): NewRouter factory for creating a new Service

The NewRouter factory will be used in our main.go to create a new router later on.

We now need to define an endpoint and a handler function for it. Our first endpoint will be /api/v1/ping and will be handled by the Calback function. Open a new file in router directory called endpoints.go and add the following code

package router

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/sirupsen/logrus"
)

// ResponseInfo for sending data back to the client
type ResponseInfo struct {
	Client     string `json:"client,omitempty"`
	SecretKey  string `json:"secret_key,omitempty"`
	Message    string `json:"message,omitempty"`
	Count      int    `json:"count,omitempty"`
	Error      string `json:"error,omitempty"`
	RemoteAddr string `json:"-"`
	URL        string `json:"-"`
	Handler    string `json:"-"`
}

// requestHandler for handling request
func (s *Service) requestHandler(responseInfo ResponseInfo, httpErr int, w http.ResponseWriter) {
	s.logger.WithFields(logrus.Fields{
		"Server":  s.Name,
		"Client":  responseInfo.RemoteAddr,
		"URL":     responseInfo.URL,
		"Address": responseInfo.RemoteAddr,
		"Handler": responseInfo.Handler,
		"Error":   responseInfo.Error,
	}).Info(responseInfo.Message)

	w.WriteHeader(httpErr)
	jsonResponse, err := json.Marshal(responseInfo)
	if err != nil {
		w.WriteHeader(400)
		fmt.Fprintf(
			w,
			err.Error(),
		)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Write(jsonResponse)
}

// Callback method for implementing a simple ping pong response
func (s *Service) Callback(w http.ResponseWriter, r *http.Request) {
	responseInfo := ResponseInfo{
		RemoteAddr: r.RemoteAddr,
		URL:        r.URL.String(),
		Handler:    "Callback",
	}

	responseInfo.Message = "pong"
	s.requestHandler(responseInfo, 200, w)
}
  • Lines (2 - 9): We defined the import block with the required package dependencies
  • Lines (12 - 21) ResponseInfo struct for creating response requests
  • Lines (24 - 46 ) requestHandler method for managing responses. This method calls logger to log any request made and sends a json response to the client
  • Lines (48 - 58) Callback method that handles ping-pong requests on /api/v1/ping endpoint.

Note: Most of the entries in ResponseInfo will be used in part 2 of this blog. Link for part 2 can be found at the end of this post.


For the scope of part-1 (this post) we now have all the code we need in the router package. In the next part we will further extend the route package

Create Go App

We are now ready to create our http app. Create a file called main.go and append the code below.

package main

import (
	"net/http"
	"os"

	"github.com/sirupsen/logrus"
	"github.com/ulfox/callback/router"
)

// Global variables
var (
	logger        = logrus.New()
	SRVER  string = "GoWebapp"
)

func die(stage, msg string) {
	logger.WithFields(logrus.Fields{
		"Component": "Main",
		"Stage":     stage,
	}).Error(msg)
	os.Exit(1)
}

func main() {
	logger.WithField("Server", SRVER).Infof("Initiated")

	service, err := router.NewRouter(SRVER, logger)
	if err != nil {
		die("NewRouter", err.Error())
	}

	err = http.ListenAndServe(":8080", service.Router)
	if err != nil {
		die("ListenAndServe", err.Error())
	}
}

Note: Please note how we import our router. If you changed ulfox with your name, do not forget to also change it in the code above.


  • Lines (2 - 7): In the code above we have our import block that has the required modules that we need
  • Lines (10 - 13): We define the logger and SRVER global variablees.

logger: We define a global logger that can be used from all functions in main.go

SRVER: This is the name of our app. We add it in the global section so it can be referenced

  • Lines (15 - 21) defined a simple die function. It takes two arguments, a name for what caused the error and the error
  • Lines (23 - 35) define the main function which on start logs an info message, defines a router and listens on port 8080

Running the app


go run main.go 
INFO[0000] Initiated                                     Callback=Callback

The http server is up and running, but we have not defined any endpoint. First stop the server by pressing ctrl c together and add the following function above main.

In main.go directly below router := mux.NewRouter().StrictSlash(true) add the following code

	service.UpdateRoutes()

This code above will update the main route with the callback endpoint handler:

  • /api/v1/ping: The handler’s endpoint
  • callback: The handler’s function
  • GET: Handler’s allowed methods

The full code now should look as the following code

package main

import (
	"net/http"
	"os"

	"github.com/sirupsen/logrus"
	"github.com/ulfox/callback/router"
)

// Global variables
var (
	logger        = logrus.New()
	SRVER  string = "GoWebapp"
)

func die(stage, msg string) {
	logger.WithFields(logrus.Fields{
		"Component": "Main",
		"Stage":     stage,
	}).Error(msg)
	os.Exit(1)
}

func main() {
	logger.WithField("Server", SRVER).Infof("Initiated")

	service, err := router.NewRouter(SRVER, logger)
	if err != nil {
		die("NewRouter", err.Error())
	}
	service.UpdateRoutes()

	err = http.ListenAndServe(":8080", service.Router)
	if err != nil {
		die("ListenAndServe", err.Error())
	}
}

Now let us run again the app and make a call


go run main.go

In a new terminal or from your browser make a request to localhost:8080/api/v1/ping

Expected server output


INFO[0000] Initiated     Callback=Callback
INFO[0001]               Callback=Callback Client="[::1]:34668" URL=/api/v1/ping

Client output


curl localhost:8080/api/v1/ping | jq
{
  "message": "pong"
}

In the next post we will extend the http service to keep count for each client’s pong and save it into a local db.

Read next part: GoAPP-CRUD-Operations