YouTube Summaries | How to Structure Your Golang (API) Projects

December 11th, 2023

Introduction:

This summary will serve to cement the learnings that I took from the video above, which discusses structure GoLang projects, specifically when building an API. I hope you find it useful too!

The outlined project directly structure is as follows:

  my-golang-project
  |-- main.go
  |-- api
  |   |-- server.go
  |   |-- handlers.go
  |-- types
  |   |-- user.go
  |-- storage
  |   |-- storage.go
  |   |-- memory.go
  |   |-- mongodb.go
  |-- util
  |   |-- util.go
  |-- api_test
  |   |-- server_test.go

Project Structure:

The video suggests a simple project structure with packages such as api, types, storage, and util. The main.go file initializes the server, handles command-line flags, and initializes a storage instance based on the chosen implementation.

Code Example:

  package main

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

    "github.com/yourusername/my-golang-project/api"
    "github.com/yourusername/my-golang-project/storage"
  )

  func main() {
    // Parse command-line flags
    listenAddr := flag.String("listen", ":3000", "Server listen address")
    flag.Parse()

    // Initialize storage (Choose either memory or mongodb implementation)
    store := storage.NewMemoryStorage()
    // store := storage.NewMongoDBStorage("mongodb://localhost:27017")

    // Initialize the server with the storage instance
    server := api.NewServer(*listenAddr, store)

    // Start the server
    fmt.Printf("Server running on %s\n", *listenAddr)
    log.Fatal(http.ListenAndServe(*listenAddr, nil))
  }

API Handling:

The api package contains the server and handlers. The Server struct manages HTTP routes, and an example route for fetching a user by ID is demonstrated. The handleUserByID function retrieves user data from storage and responds with a JSON representation.

Code Example:

  package api

  import (
    "fmt"
    "net/http"

    "github.com/yourusername/my-golang-project/storage"
    "github.com/yourusername/my-golang-project/types"
  )

  type Server struct {
    listenAddr string
    store      storage.Storage
  }

  func NewServer(listenAddr string, store storage.Storage) *Server {
    return &Server{
      listenAddr: listenAddr,
      store:      store,
    }
  }

  func (s *Server) handleUserByID(w http.ResponseWriter, r *http.Request) {
    // Example: Get user by ID from storage
    user := s.store.Get(1)

    // Example: Encode user as JSON and write to response
    fmt.Fprintf(w, "{\"id\": %d, \"name\": \"%s\"}", user.ID, user.Name)
  }

  func (s *Server) Start() error {
    // Example: Handle "/user" endpoint
    http.HandleFunc("/user", s.handleUserByID)

    // Start server
    return http.ListenAndServe(s.listenAddr, nil)
  }

Type Definitions:

The types package holds type definitions for entities like the User struct, which model data structures throughout the application.

Code Example:

  package types

  type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
  }

Storage Layer:

The storage package provides an interface (Storage) for data storage and specific implementations, such as MemoryStorage. The chosen storage implementation is initialized in the main file. The video highlights the simplicity of the in-memory storage example.

Code Examples:

storage/storage.go

  package storage

  import "github.com/yourusername/my-golang-project/types"

  type Storage interface {
    Get(id int) types.User
    // Add other storage methods here
  }

storage/memory.go:

  package storage

  import "github.com/yourusername/my-golang-project/types"

  type MemoryStorage struct{}

  func NewMemoryStorage() *MemoryStorage {
    // Initialization logic for memory storage
    return &MemoryStorage{}
  }

  func (m *MemoryStorage) Get(id int) types.User {
    // Example: In-memory implementation
    return types.User{ID: id, Name: "Foo"}
  }

Utility Functions:

A util package is suggested for utility functions used across the application. An example includes a RoundToDecimals function for rounding floating-point numbers.

Code Example:

  package util

  import "math"

  func RoundToDecimals(val float64) float64 {
    return math.Round(val*100) / 100
  }

Testing:

Testing is encouraged by creating a separate test file (api_test/server_test.go). The video demonstrates a test for the handleUserByID function using Go’s testing package.

Code Example:

  package api_test

  import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/yourusername/my-golang-project/api"
  )

  func TestHandleUserByID(t *testing.T) {
    // Create a test server
    server := api.NewServer(":3000", nil)

    // Create a request to the "/user" endpoint
    req, err := http.NewRequest("GET", "/user", nil)
    if err != nil {
      t.Fatal(err)
    }

    // Create a response recorder to record the response
    rr := httptest.NewRecorder()

    // Call the handler function
    server.HandleUserByID(rr, req)

    // Check the status code
    if status := rr.Code; status != http.StatusOK {
      t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusOK)
    }

    // Check the response body (assuming the expected JSON response)
    expected := `{"id": 1, "name": "Foo"}`
    if rr.Body.String() != expected {
      t.Errorf("Handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
    }
  }

The proposed structure aligns with Go’s simplicity, promoting accessibility for developers and facilitating long-term maintainability.