Elijah's Blog

Go Web Application Structure - Part 1
Go Web Application Structure - Part 1

Originally posted at https://aaf.engineering/go-web-application-structure-pt-1/

Introduction

When writing Go applications, especially web applications, there isn't much documentation on how to do it the right way. This is both a blessing and a curse. One of the best things about Go is that it comes with batteries included for the most part so you don't need to use many, if any, external packages if you don't want to. This is especially true for web applications. In other languages, such as Python and Ruby, there are many different options for web application frameworks. Some of these options are Django, Rails, Flask, & Sinatra. Go has a few frameworks, Revel, Beego, & Gin to name a few, however, it's generally advised to not use a web framework when writing a Go web application. Most programmers coming from languages like Python and Ruby might find this strange or even daunting; many will fall back to using one of the aforementioned Go web frameworks. This and the following blog posts attempt to give a full example of how to write a Go web application without using a web framework.

At The Alliance, we use Golang and GraphQL for our API. Read more about our API here.

Application Layers

business_layer

API Layer

This is a lightweight interface to our business logic. When a client interacts with our application, it will do so through an API. Common interfaces are GraphQL, gRPC, and REST. This layer should contain minimal (if any) application logic. This layer is only responsible for translating the data returned from the business logic layer into a consumable format for clients.

Business Logic Layer

This is where business logic (including authorization) is handled. The term "business logic" is often tossed around so much that it has lost its meaning. This StackOverflow article has a great definition:

Business logic or domain logic is that part of the program which encodes the real-world business rules that determine how data can be created, displayed, stored, and changed. It prescribes how business objects interact with one another, and enforces the routes and the methods by which business objects are accessed and updated.

Often times developers incorrectly place their business logic in their API controllers. This should be completely separate, in the following section we define a separate module for our business logic. A concrete example of this is, let's say we have a simple TODO application. We have an API endpoint that allows users to create a new TODO. We may want to validate a couple of things; for example, the provided TODO text isn't too long & the user hasn't hit their daily limit. All this validation logic should be handled in the business layer.

Authorization & authentication are also handled in this layer. This means determining who the current user is when a request is sent to your server (authentication). This determination can be handled in many different ways, ranging from cookies to authentication tokens. Any authorization logic surrounding who has access to what, should be handled in this layer; authorization errors that happen should be handled appropriately in your API layer. Concretely, this means that when an error is thrown or returned in a method here, your API should catch it and return a 403 or 401 error.

Persistence Layer

This is where data is persisted to a database or other store ("database" from here on). After a request has been authorized and/or validated we need to make some request to our database. Generally, there are four common operations we make to a database, they are commonly referred to as CRUD: Create Read Update Delete. Most databases support these operations in one or many forms. In this layer, our business logic layer makes these requests to our database. The only thing this layer should be responsible for is database operations; there shouldn't be any business logic.

Folder structure and project set up

A Go project looks quite different than a Django or Rails project. Files are grouped by their application layer rather than business application.

Our top-level folders will be as follows:

  • api (api controllers)
  • app (business logic)
  • cmd (command line methods)
  • db (database operations)
  • models (database models)

To get started you can type the following commands:

mkdir -p $(go env GOPATH)/src/github.com/<username>/todos
cd $(go env GOPATH)/src/github.com/<username>/todos
dep init

The command line

Many web application frameworks give you a way to interact with it through the command line. Django gives you manage.py, Rails gives you bin/rails, we will create our own. The Golang flags package is already pretty good for writing basic command line applications, but we will opt to use a package called cobra. The advantage of using cobra is that it gives you an easy way to write and structure commands. For example, each command we have will have a separate Go file in the cmd directory.

A quick word on configuration

For application configuration, we will use a package called viper which can be used in accordance with The Twelve-Factor App guidelines. Viper allows for configuration with a config file or with environment variables.

The root command

To use cobra, we will configure our "root" command. This will be used to register all other commands we create. It will also be where we load our application configuration. This file should be cmd/root.go and will have the following contents:

package cmd

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var rootCmd = &cobra.Command{
    Use:   "todos",
    Short: "Todo Web Application",
    Run: func(cmd *cobra.Command, args []string) {
        cmd.Usage()
    },
}

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

var configFile string

func init() {
    cobra.OnInitialize(initConfig)
    rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is config.yaml)")
}

func initConfig() {
    if configFile != "" {
        viper.SetConfigFile(configFile)
    } else {
        viper.SetConfigName("config")
        viper.AddConfigPath(".")
        viper.AddConfigPath("/etc/todos")
        viper.AddConfigPath("$HOME/.todos")
    }

    viper.AutomaticEnv()

    if err := viper.ReadInConfig(); err != nil {
        fmt.Printf("unable to read config: %v\n", err)
        os.Exit(1)
    }
}

The last thing we need to have a fully working Go application is a main package. In the root directory, create a main.go file with the following contents:

package main

import "github.com/theaaf/todos/cmd"

func main() {
	cmd.Execute()
}

This file and function within tells Go what the "main" entry point into our application is.

Now we can run go build -o todos . and then ./todos to get some usage output.

Usage:
  todos [flags]

Flags:
      --config string   config file (default is config.yaml)
  -h, --help            help for todos

The version command

Next, we can add a simple version command to print the application's version. This should go in cmd/version.go.

package cmd

import (
    "fmt"

    "github.com/spf13/cobra"
)

func init() {
    rootCmd.AddCommand(versionCmd)
}

var versionCmd = &cobra.Command{
    Use:   "version",
    Short: "Print the version number",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Todos v1.0")
    },
}

Now we can run ./todos version and get the version number!

Todos v1.0

Conclusion

In this post, we saw the high-level folders needed for a Go web application, how to set up a command line interface, and writing a simple version command. The next parts will dive into using gorilla/mux for routing for how we can serve the web application, interacting with the database (including migrations), and finally how to separate business logic from our API.


  • Part 1: Web Application Structure
  • Part 2: Routing/Serving
  • Part 3: The Database
  • Part 4: Database Migrations & Business Logic

Code: https://github.com/tizz98/todos

Cover photo by Juan Pablo Arenas from Pexels.

Newer
Older