Building a REST API in Golang

Image credits: Photo by ThisIsEngineering from Pexels

REST APIs are one of the most common architecture patterns in today’s distributed software systems. Therefore, I want to give you an overview of essential REST concepts and illustrate them with an example API in Golang.

What is a REST API?

  • API, or Application Programming Interface, is a generic term for a bridge-like layer between software components. It allows individual parts of a software system to communicate and interact with each other.
  • REST stands for Representational State Transfer. It describes a specific style of designing an API for communication between distributed applications over a network.
  • REST APIs are commonly used in software systems of various sizes. You can find them in simple web or mobile applications, as well as in complex distributed microservice architectures.
  • Communication via REST is performed synchronously between two participants. Hence, it is a simple way of asking for data from another system and then almost immediately using that data, e.g. for displaying it in a graphical user interface.
  • For some scenarios, asynchronous architecture patterns can be more suitable. This can be the case when data needs to be distributed to multiple services or when data preparation takes a lot of time.
  • Exchanging data via REST is language-neutral, which makes it very compatible with all kinds of software systems. Often, basic HTTP and data serialization capability is sufficient (see building blocks below).

Building blocks

  • Communication happens between a server and a client. The server provides the REST API, and a client can interact with it.
  • Although not required by REST, HTTP is typically used as the underlying protocol. Hence, we typically need an HTTP server.
  • REST is about interacting with resources. Therefore, resources are the subjects in our API design, while HTTP methods and URL paths describe the action that is performed.
  • We need to serialize data over the wire, as REST is designed for communicating over a network. REST does not require any specific data format. The most commonly used format is JSON, though, as it is both simple and human-readable.

The Todo list API

  • For illustration purposes, we will look at a simple REST API written in Golang.
  • Our resources are Todo items. A Todo item describes a task and can be marked as complete or incomplete:
{
  "title": "study for math exam",
  "complete": false
}
  • The complete Todo list can be fetched with a GET /todo/all request. It is a read-only operation, which is important for GET requests. It should not modify any state on the server side.
[
  {
    "title": "take the thrash out",
    "complete": false
  },
  {
    "title": "study for math exam",
    "complete": false
  },
  {
    "title": "watch golang tutorial",
    "complete": true
  }
]
  • A new Todo item can be added by sending a POST /todo request and transferring the new Todo item in our HTTP request body. Hence, we create a new Todo item resource. Changing server side state is typical for a POST operation.
  • In addition, there are several endpoints that work on a specific Todo list item. In all these cases, an {index} parameter is used in the URL path. It marks the position of an existing Todo item (derived from the current list).
    • A specific Todo item can be fetched by sending a GET /todo/{index} request. Again, it is a read-only operation.
    • Existing Todo items can be updated by sending a PUT /todo/{index} request and transferring the updated Todo item in the HTTP request body. PUT is typically used to create or replace resources. PUT operations should be idempotent, meaning that calling them once must have the same effect as calling them multiple times (using the same parameters). In our case, we use it for replacing existing Todos items only.
    • Todo items can be removed from the list by sending a DELETE /todo/{index} request. As the HTTP verb suggests, this deletes a specific resource.

Implementation in Go

HTTP server

Starting an HTTP server in Golang is very simple and straight-forward. Below, we define a simple endpoint /hello that returns a greeting in its response body.

package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/hello", func(writer http.ResponseWriter, 
	                               request *http.Request) {
		fmt.Fprintf(writer, "Hello world!\n")
	})
	http.ListenAndServe(":8090", nil)
}

After starting our program with go run main.go we can query this endpoint, e.g. by using curl:

$ curl localhost:8090/hello
Hello world!

In our hello world example, we passed nil to http.ListenAndServe. This parameter is called a handler, and it defines how incoming requests are processed. Passing nil means we want to use the default handler. Go creates it automatically for us, which makes it very convenient for simple use cases. We just had to add a handler function for the /hello endpoint, and the default handler took care of dispatching such requests to our function.

Request handling

For our REST API it makes sense to have more control and flexibility about request handling. As an example, we want to define which HTTP methods are required for certain operations. Moreover, we want to have parameters in our URL path, e.g. for identifying which items to update or remove. While this is all possible with standard handlers, it requires more effort for implementation. A simple and more powerful approach is to use Golang’s ServeMux type. Mux stands for Multiplexer meaning that it dispatches incoming requests to our actual handling logic by looking at the URL path and HTTP verb. For our API, the ServeMux is set up like this:

func makeMux() *http.ServeMux {
	mux := http.NewServeMux()

	// Important: Make sure your Go version is 1.22.1 or higher as
	// there have been multiple changes to the mux routing.
	// Note the end marker {$} to match exactly root in the first case.
	mux.HandleFunc("GET /{$}", handleWelcome)
	mux.HandleFunc("GET /todo/{index}", handleGet)
	mux.HandleFunc("GET /todo/all", handleGetAll)
	mux.HandleFunc("POST /todo", handlePost)
	mux.HandleFunc("PUT /todo/{index}", handlePut)
	mux.HandleFunc("DELETE /todo/{index}", handleDelete)

	return mux
}

func main() {
	mux := makeMux()
	fmt.Println("Listening for requests...")
	http.ListenAndServe(":8090", mux)
}

Data model

Next, let’s take a look at our data model. As you can see below, we are simply adding json tags to the field names to specify the desired field name (lowercase here). It’s important to know that only exported fields will be written to and read from JSON. Therefore, all included fields must start with a capital letter.

type Todo struct {
	Title     string `json:"title"`
	Completed bool   `json:"completed"`
}

// Provide some initial example todos.
var allTodos = []Todo{
	{Title: "study for math exam"},
	{Title: "take the trash out"},
	{Title: "watch golang tutorial"},

Reading and writing JSON

Reading and writing JSON is pretty straight-forward, too. Go offers the standard functions json.Marshal for writing and json.Unmarshal for reading. Here is the implementation of our GET /todo/all endpoint showing us how to write a Go type as JSON to an HTTP response:

// handeGetAll writes the complete todo list as json to the response body.
func handleGetAll(writer http.ResponseWriter, request *http.Request) {
	data, err := json.Marshal(allTodos)

	if err != nil {
		handleError(writer, "Failed to create response body",
		            http.StatusInternalServerError)
		return
	}

	writer.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(writer, "%s", data)
}

Reading is the inverse process, as we can see below. Taking a byte slice from our request body, we pass it to json.Unmarshal for reading it into a Todo struct instance. It is important to pass a pointer so Unmarshal can set the fields of our struct accordingly.

// handlePost parses a new todo item from the request body json and
// adds it to the list.
func handlePost(writer http.ResponseWriter, request *http.Request) {
	body, err := ioutil.ReadAll(request.Body)
	if err != nil {
		handleError(writer, "Failed to read request body",
		            http.StatusBadRequest)
		return
	}

	var todo Todo

	err = json.Unmarshal(body, &todo)
	if err != nil {
		handleError(writer, "Failed to parse request body",
		            http.StatusBadRequest)
		return
	}

	allTodos = append(allTodos, todo)
	writer.WriteHeader(http.StatusCreated)
}

URL parameters

For our endpoint GET /todo/{index} it is as simple as shown below. The returned value is a string and needs to be parsed into an integer and validated by us. An empty string will be returned if there is no such path value.

indexStr := request.PathValue("index")
// Parse int from raw path value, perform bounds checking etc.

Query parameters

We can extend our API with an optional query parameter and make GET /todo/all?completed=false return only open tasks (i.e. not completed yet). The URL part after the question mark is called a query parameter and may contain a list of simple key-value pairs.

queryParams := request.URL.Query()
values, found := queryParams["completed"]
// Parse bool from first query value string values[0].

Status codes and error messages

Error handling is an important part of API implementations. The first indicator of success or error is the HTTP status code. If none is specified, Go will set a response status to 200 (ok) by default. This means that everything worked fine. Sometimes, we want to set different codes, likes 201 (created) for our POST case, which creates a new Todo. We can do it like this:

writer.WriteHeader(http.StatusCreated)

Similarly, we can pass a status like http.BadRequest if for example the client passed invalid data to us. For error cases, it is generally recommended to give more context to the client as it helps in understanding the problem.

message := "Index must be greater than zero and smaller than todo list length"
http.Error(writer, message, http.StatusBadRequest)

Summary

By putting together the building blocks above, we created a simple but capable REST API in Golang. Note that we never had to use any third-party libraries. The standard library offers all we need: An HTTP server, JSON conversion and request dispatching. Understanding and using these features is pretty straightforward. This is typical for Golang and often more complicated in other languages.

We discussed all important concepts for our REST API. However, if you are interested in the full source code, including automated tests, feel free to take a look at my rest-api-todos repository.

Bastian Isensee
Bastian Isensee
Software Engineer (Freelancer)

Quality-driven Software Engineer focused on business needs, knowledge sharing, FinTechs, Golang and Java