Simple REST API with Go

Let's create a simple REST API with Go or Golang to understand CRUD (Create Read Update Delete) operations with the most commonly used HTTP methods; POST, GET, PUT, PATCH, and DELETE.

My team assigned me to create a service that will handle millions of requests. I decided to create it with Go. The most reason is fast. You code this simple language, compile it, then run it. The compiled one is the native program, create a service on it, then run it on the operating system. It has no dependency.

Some high tech companies or startups have some services that run on Go. It is worth to learn. This tutorial is to make you a little familiar with Go. What will you create is a simple REST API for articles or a blog. Here is my updated article about a simple explanation of REST API. So, it is a nice start if you are new to Go.

List of contents:

  1. Tools
  2. Packages
  3. Data
  4. Index
  5. Create
  6. Read
  7. Replace
  8. Modify
  9. Remove
  10. Main
  11. Conclusion

Tools

First, you need to install Go on your system. You can go to https://golang.org/doc/install then follow the instructions. The last is the absolutely text editor. I was using VS Code while writing this tutorial.

Packages

package main

import (
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"
    "strconv"

    "github.com/gorilla/mux"
)

First, you need to write your package. It is the same as the main function name, main(), and the file name, main.go. You don't need to write the packages that you need early because VS Code can do that on the fly. It can suggest you related extensions that you need to install. It makes your code easy and simple.

Data

type article struct {
    Title       string `json:"title"`
    Description string `json:"description"`
}

var articles = map[int]article{
    1: {
        "Simple REST API with Go",
        "Let's create a simple REST API with Go.",
    },
}

The most crucial fields of the article are the title and description. There is also some defined article. In the real situation, you need to get the data from the database. They are enough for our tutorial. The struct is a collection of fields. I chose the map, key-value data structure, to easily access and define the data in constant time.

Index

func index(res http.ResponseWriter, req *http.Request) {
    res.Header().Set("Content-Type", "application/json")
    json.NewEncoder(res).Encode(articles)
}

Just print all articles as JSON.

Create

func create(res http.ResponseWriter, req *http.Request) {
    newArticle := article{}
    reqBody, err := ioutil.ReadAll(req.Body)

    if err != nil {
        log.Fatal(err)
    }

    json.Unmarshal(reqBody, &newArticle)
    articles[len(articles)+1] = newArticle

    res.Header().Set("Content-Type", "application/json")
    json.NewEncoder(res).Encode(articles)
}

Parsing JSON request to the article struct, add it to the list of articles, then print them as JSON. The log.Fatal is to print the error and immediately abort the program. The json.Unmarshal parses the request JSON body to the defined struct.

Read

func read(res http.ResponseWriter, req *http.Request) {
    articleParam := mux.Vars(req)["article"]
    articleID, err := strconv.Atoi(articleParam)

    if err != nil {
        log.Fatal(err)
    }

    if articles[articleID].Title == "" &&
        articles[articleID].Description == "" {
        res.WriteHeader(http.StatusNotFound)
        return
    }

    res.Header().Set("Content-Type", "application/json")
    json.NewEncoder(res).Encode(
        map[int]article{
            articleID: articles[articleID],
        },
    )
}

Get the parameter article which is the article ID. Convert it from string to integer. Check wheater the article exists or not. Return 404 if not exist. Print the article if exist.

Replace

func replace(res http.ResponseWriter, req *http.Request) {
    articleParam := mux.Vars(req)["article"]
    articleID, err := strconv.Atoi(articleParam)

    if err != nil {
        log.Fatal(err)
    }

    if articles[articleID].Title == "" &&
        articles[articleID].Description == "" {
        res.WriteHeader(http.StatusNotFound)
        return
    }

    updatedArticle := article{}
    reqBody, err := ioutil.ReadAll(req.Body)

    if err != nil {
        log.Fatal(err)
    }

    json.Unmarshal(reqBody, &updatedArticle)
    articles[articleID] = updatedArticle

    res.Header().Set("Content-Type", "application/json")
    json.NewEncoder(res).Encode(articles)
}

Here is we replace the entire article with the new one even only some fields updated.

Modify

func modify(res http.ResponseWriter, req *http.Request) {
    articleParam := mux.Vars(req)["article"]
    articleID, err := strconv.Atoi(articleParam)

    if err != nil {
        log.Fatal(err)
    }

    if articles[articleID].Title == "" &&
        articles[articleID].Description == "" {
        res.WriteHeader(http.StatusNotFound)
        return
    }

    newArticle := article{}
    updatedArticle := article{}
    reqBody, err := ioutil.ReadAll(req.Body)

    if err != nil {
        log.Fatal(err)
    }

    json.Unmarshal(reqBody, &updatedArticle)

    if updatedArticle.Title != "" {
        newArticle.Title = updatedArticle.Title
    } else {
        newArticle.Title = articles[articleID].Title
    }

    if updatedArticle.Description != "" {
        newArticle.Description = updatedArticle.Description
    } else {
        newArticle.Description = articles[articleID].Description
    }

    articles[articleID] = newArticle

    res.Header().Set("Content-Type", "application/json")
    json.NewEncoder(res).Encode(articles)
}

Here is we only update some updated fields of the article.

Remove

func remove(res http.ResponseWriter, req *http.Request) {
    articleParam := mux.Vars(req)["article"]
    articleID, err := strconv.Atoi(articleParam)

    if err != nil {
        log.Fatal(err)
    }

    if articles[articleID].Title == "" &&
        articles[articleID].Description == "" {
        res.WriteHeader(http.StatusNotFound)
        return
    }

    // Delete the entry from the map.
    // In the real situation,
    // you need to delete it from the database.
    delete(articles, articleID)

    // Mostly in the delete operation,
    // return status ok is enough if succeed.
    res.WriteHeader(http.StatusOK)

    // Feel free to open this comment,
    // to check that the entry is deleted.
    // res.Header().Set("Content-Type", "application/json")
    // json.NewEncoder(res).Encode(articles[articleID])
}

Delete or remove the article. The status must be OK even if there is no response data.

Main

func main() {
    router := mux.NewRouter()
    version := "/v1"
    path := "/articles"

    router.HandleFunc(version+path, index).Methods("GET")
    router.HandleFunc(version+path, create).Methods("POST")
    router.HandleFunc(version+path+"/{article}", read).Methods("GET")
    router.HandleFunc(version+path+"/{article}", replace).Methods("PUT")
    router.HandleFunc(version+path+"/{article}", modify).Methods("PATCH")
    router.HandleFunc(version+path+"/{article}", remove).Methods("DELETE")

    log.Fatal(http.ListenAndServe(":80", router))
}

We route those functions to some HTTP methods even the URL address is the same. That is the REST API technically.

Conclusion

Easy and simple right? You can use go run main.go for every time you have made some changes and go build for the final one. Here is the final code along with a Postman collection ready for you to try, https://github.com/aristorinjuang/go-rest-api. In the end, it brought an experience of Go to you. We had implemented CRUD operations to the REST API and explained the most commonly used HTTP methods of the REST API, especially PUT vs PATCH.