How-To Guides

Building Scalable REST APIs with Go: A Developer's Guide

Introduction

Go, often referred to as Golang, has rapidly become a preferred choice for building high-performance backend services. Its concurrency model, static typing, and exceptional execution speed make it an ideal candidate for modern microservices architectures. While frameworks like Gin or Echo offer convenience, understanding how to build REST APIs using Go’s standard library is crucial for mastering the language’s underlying mechanics. This guide walks you through creating a basic, functional REST API using only the standard library, providing a solid foundation before you move on to third-party tools.

Setting Up the Project Structure

Before writing code, initialize your Go module. Open your terminal and run the following command in your project directory:

go mod init my-rest-api

This command creates a go.mod file, which manages your project’s dependencies. For this tutorial, we will rely solely on Go’s built-in net/http and encoding/json packages.

Defining Data Structures

Let’s start by defining a simple resource: a “Task.” We’ll use a struct to represent this data. The `json` tags are essential for marshaling and unmarshaling JSON data to and from HTTP requests and responses.

package main

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

type Task struct {
    ID     string `json:"id"`
    Title  string `json:"title"`
    Done   bool   `json:"done"`
}

Implementing HTTP Handlers

Go’s net/http package uses the HandlerFunc interface. A handler is simply a function with the signature func(http.ResponseWriter, *http.Request). Let’s implement a handler to list all tasks and another to create a new one.

The List Handler

This handler reads a global slice of tasks and marshals it into a JSON response. It’s important to set the Content-Type header correctly so clients know how to interpret the payload.

var tasks []Task

func listTasks(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(tasks)
}

The Create Handler

This handler listens for POST requests. It decodes the incoming JSON body into a new Task struct, appends it to our slice, and returns the created object with a 201 Created status code.

func createTask(w http.ResponseWriter, r *http.Request) {
    var newTask Task
    // Decode the JSON body from the request
    err := json.NewDecoder(r.Body).Decode(&newTask)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // Add ID logic here (e.g., using uuid package)
    newTask.ID = "1" 
    tasks = append(tasks, newTask)

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(newTask)
}

Routing and Server Configuration

Now that our handlers are defined, we need to map them to specific URL paths. In Go, we use http.HandleFunc or http.NewServeMux for more complex routing.

func main() {
    // Initialize the tasks slice
    tasks = []Task{
        {ID: "1", Title: "Learn Go", Done: false},
    }

    // Register routes
    http.HandleFunc("/tasks", listTasks)
    http.HandleFunc("/tasks/create", createTask)

    // Start the server on port 8080
    log.Println("Server starting on :8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}

Conclusion

Building REST APIs with Go’s standard library is straightforward and powerful. By leveraging net/http and encoding/json, you gain fine-grained control over your server without the overhead of large frameworks. While this example is basic, it demonstrates core concepts like JSON marshaling, status codes, and request handling that apply to any Go web application. For production applications, consider adding middleware for logging, authentication, and CORS, or switch to a router like Chi or Gin for more complex routing needs. Happy coding!

Share: