Introduction to Go - Create your first REST API

Principal Software Engineer at FWD.hr. FOSS enthusiast. Learning every day :)
Search for a command to run...

Principal Software Engineer at FWD.hr. FOSS enthusiast. Learning every day :)
Hi Azar, thanks for your comment. While mux does support protobuf to some level (https://medium.com/swlh/supercharge-your-rest-apis-with-protobuf-b38d3d7a28d3), HTTP/2|3 is questionable and I did not test this out so far.
Hi Razvan, thanks for you interest in my article :) As with all languages and frameworks it all depends on the development speed (learning curve) and your personal needs. Go offers a wide range of frameworks/libs like Mux, Fiber, Gin and Echo. The best way I can reply to you is to take a look at the numbers here:
https://go.libhunt.com/compare-fiber-vs-mux
I also suggest that you take a look at all the other libs I mentioned here (for example if performance speed is a key component for your needs - take a look at Echo -> https://echo.labstack.com/cookbook/crud/) and find the one that suits you best for your needs :)
In this article we will create a simple exploit with Python. NOTE: Since the entire code snippet is not that long, no final code will be provided on GitLab this time. Foreword This article is STRICTLY for educational purposes. All the information in...

Is my code good enough? It works, but can it be more optimized? Can I shorten it a bit? Is it readable? Well - we all ask these kinds of questions at all stages of our developer careers. I will share some easy tips with you on how you can improve you...

If you ever used Python, most likely you used context managers without even knowing what they are and what they do or how they work - you just used them as per the documentation of some 3rd party package. In this article, I will try to explain what e...

Last time I showed you how to build a simple book store REST API. This time I want to take it a step further and show you how to build an (almost) production-ready API with real database storage instead of an in-memory mock that we used. Also, I will...

Whenever we are building something with Python, most of the time we rely on third-party packages so that we don't have to reinvent the wheel for some simple things like sending an HTTP request. By doing so we lose control of how safe we are with that...

Go (Golang) is the new kid on the block in terms of the recent popularity surge. It is small, stable, simple to use and learn, fast, compiled (native code), and heavily used in cloud tools and services (Docker, Kubernetes, ...). There is no reason not to take it for a spin considering all of the perks that come with it. In this tutorial, we will build a simple book store REST API.
NOTE: In a hurry? Get the final code here.
Before we start, some of the prerequisites we will use:
Since we will build a complete API solution, I highly recommend that you take a look at Go tour.
You should have Go installed and ready by now. Open up your favorite IDE for Go (Visual Studio Code, GoLand, ...) and create a new Go project. As I mentioned earlier, the idea is to build a simple REST API for book store management by using Mux. Once you created your blank project, create the following structure inside of it:
├── main.go
└── src
├── app.go
├── data.go
├── handlers.go
├── helpers.go
└── middlewares.go
This is the right time to talk about Go modules and packages. If you are familiar with Python, you might get an idea of what those are since they operate similarly.
The best way to describe a Go package is that it's a collection of source files in the same directory that are compiled together as a reusable unit. That means that all files that serve a similar purpose should be put inside one package. As per our structure above - src is one of our packages.
Go module is a collection of Go packages along with their dependencies, meaning that one module can consist of multiple packages. You can think of our whole application as a Go module for easier understanding.
Let's create our module by executing this command in our project root directory:
go mod init bookstore
You should see a new file inside your root directory called go.mod - so far so good :)
It is time to start building our application. Open up your main.go file and insert the following code inside of it:
package main
import "bookstore/src"
func main() {
src.Start()
}
We declared our main Go package (package main) and imported our src package along with the module bookstore prefix. Inside the function main() we will run the Start() function of package src. This is the only responsibility of our entry point file (main.go) - fire up the API.
Now we need to create our API router (Mux) and configure it by creating some endpoints and their handlers. Inside of your src package open up app.go and insert the following code inside of it:
package src
import (
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"log"
"net/http"
"os"
)
func Start() {
router := mux.NewRouter()
router.Use(commonMiddleware)
router.HandleFunc("/book", getAllBooks).Methods(http.MethodGet)
router.HandleFunc("/book", addBook).Methods(http.MethodPost)
router.HandleFunc("/book/{book_id:[0-9]+}", getBook).Methods(http.MethodGet)
router.HandleFunc("/book/{book_id:[0-9]+}", updateBook).Methods(http.MethodPut)
router.HandleFunc("/book/{book_id:[0-9]+}", deleteBook).Methods(http.MethodDelete)
log.Fatal(http.ListenAndServe("localhost:5000", handlers.LoggingHandler(os.Stdout, router)))
}
As you can see above - we declared that app.go is part of the src package and it contains the Start() function that we used inside our main.go file. We also included two external modules that we need to install as dependencies for our bookstore module.
Execute the following commands in your terminal:
go get github.com/gorilla/handlers
go get github.com/gorilla/mux
Your go.mod file should have synced as well and it should now look something like this:
module bookstore
go 1.17
require (
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
)
require github.com/felixge/httpsnoop v1.0.1 // indirect
Let's take a deeper look at our Start() function. First, we declared a new Mux router variable which will be responsible for the routing and handling of requests across our API. Then we told Mux that we want to include a middleware that will execute upon each request that comes to our API with the line:
router.Use(commonMiddleware)
More about middleware a bit later. If we continue to analyze our code we can finally see where we create endpoints along with handlers (callback functions) and some primitive validations. For example:
router.HandleFunc("/book/{book_id:[0-9]+}", updateBook).Methods(http.MethodPut)
This endpoint will fire up once a user hits our server at the /book/123 (or any other number) path with a PUT method. It will then pass the request to the updateBook handler function for further processing. The book_id variable has to be a number as we specified a simple validation after the variable name declaration.
Finally, we will run our server on the specific host and port combination and make it log everything to our terminal:
log.Fatal(http.ListenAndServe("localhost:5000", handlers.LoggingHandler(os.Stdout, router)))
As we all know - REST APIs mostly use JSON when taking requests and returning responses. That is communicated to our browsers/HTTP clients by making use of Content-Type headers. Since our API will only be using JSON-represented data, we can make use of a middleware that will make sure our content type is always set to JSON.
As mentioned earlier, the Start() method of app.go contains this line:
router.Use(commonMiddleware)
Let's open up our middlewares.go file and create the needed function:
package src
import "net/http"
func commonMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json; charset=utf-8")
w.Header().Set("x-content-type-options", "nosniff")
next.ServeHTTP(w, r)
})
}
Once the user hits any of the endpoints that we registered in our Start() function to the Mux router, the middleware will intercept that request and add the two headers that we specified inside of our commonMiddleware function. It will then pass the modified request further to the handling function of the requested endpoint OR to another middleawre (in case we need more than one middleware).
Since we won't be using any data storage services (database, cache, ...) we need to have some sort of static data. Also, we will create a data type for custom responses, which I will explain later on.
Open up the data.go inside the src package and put the following inside of it:
package src
type Book struct {
Id int `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
Genre string `json:"genre"`
}
var booksDB = []Book{
{Id: 123, Title: "The Hobbit", Author: "J. R. R. Tolkien", Genre: "Fantasy"},
{Id: 456, Title: "Harry Potter and the Philosopher's Stone", Author: "J. K. Rowling", Genre: "Fantasy"},
{Id: 789, Title: "The Little Prince", Author: "Antoine de Saint-Exupéry", Genre: "Novella"},
}
We just created a data structure that will hold the information needed for a single book inside our API. We also created json tags which will translate the field names to its JSON representation if the data type will be passed as JSON.
We also created a primitive book storage system (in memory) with some initial books data (booksDB).
Add this code below the one from the above:
type CustomResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Description string `json:"description,omitempty"`
}
var responseCodes = map[int]string {
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
409: "Conflict",
422: "Validation Error",
429: "Too Many Requests",
500: "Internal Server Error",
}
We just made a new data structure that will unify the errors/responses that our API will return. More on this later on.
We will need some helpers to get the most of our API. For example, we will need to check if a book with a given ID exists (adding a new book, modifying an existing book). We will also need to delete a book with a given ID (delete book). We will also create a helper that will return a custom JSON response for a given HTTP status code.
Open up helpers.go inside of src package and insert the following inside:
package src
import (
"encoding/json"
"net/http"
)
func removeBook(s []Book, i int) []Book {
if i != len(s)-1 {
s[i] = s[len(s)-1]
}
return s[:len(s)-1]
}
func checkDuplicateBookId(s []Book, id int) bool {
for _, book := range s {
if book.Id == id {
return true
}
}
return false
}
func JSONResponse(w http.ResponseWriter, code int, desc string) {
w.WriteHeader(code)
message, ok := responseCodes[code]
if !ok {
message = "Undefined"
}
r := CustomResponse{
Code: code,
Message: message,
Description: desc,
}
_ = json.NewEncoder(w).Encode(r)
}
The removeBook function will go over our Book slice and look for index value i. If it is not the last element of the slice, it will move it to the end of the slice and return a new slice without it (avoid the last element).
checkDuplicateBookId function will return a bool value (true or false) depending on if the given id exists inside of the Book slice.
The JSONResponse function is responsible for making use of the CustomResponse and responseCodes we created earlier on. It will return a CustomResponse JSON representation with the status code and message that responseCodes will provide. This way we will avoid having different messages across our API for the same HTTP status codes (eg. 400: Bad Request and 400: Invalid Data).
If you made it so far, congrats :)
Let's jump to the final part - putting the endpoint handlers together. Open up your handlers.go and let's type some code inside of it.
package src
import (
"encoding/json"
"github.com/gorilla/mux"
"net/http"
"strconv"
)
func getBook(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bookId, _ := strconv.Atoi(vars["book_id"])
for _, book := range booksDB {
if book.Id == bookId {
_ = json.NewEncoder(w).Encode(book)
return
}
}
JSONResponse(w, http.StatusNotFound, "")
}
Not much is going on here. We get the passed variables from our Mux router (in our case it is just one variable that we described in app.go for this handler - book_id) and we convert it from string to int value. We iterate over our booksDB and look for the matching book ID. If it exists, we return it - if not, we return the 404: Not Found error.
func getAllBooks(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(booksDB)
}
Simple one eh? Convert the booksDB slice to JSON and return it to the user.
func addBook(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var b Book
err := decoder.Decode(&b)
if err != nil {
JSONResponse(w, http.StatusBadRequest, "")
return
}
if checkDuplicateBookId(booksDB, b.Id) {
JSONResponse(w, http.StatusConflict, "")
return
}
booksDB = append(booksDB, b)
w.WriteHeader(201)
_ = json.NewEncoder(w).Encode(b)
}
Since this one triggers on POST method, the user must provide the JSON data inside the request body that will match the Book structure:
{
"id": 999,
"title": "SomeTitle",
"author": "SomeAuthor",
"genre": "SomeGenre"
}
Once we decode and validate the JSON body against our Book struct (if it fails we will return the 400: Bad Request error), we need to check if the book with the same ID already exists. If that is the case, we return the 409: Conflict error back. Otherwise, we will append our booksDB with the user-provided book and return its JSON representation to the user.
func updateBook(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bookId, _ := strconv.Atoi(vars["book_id"])
decoder := json.NewDecoder(r.Body)
var b Book
err := decoder.Decode(&b)
if err != nil {
JSONResponse(w, http.StatusBadRequest, "")
return
}
for i, book := range booksDB {
if book.Id == bookId {
booksDB[i] = b
_ = json.NewEncoder(w).Encode(b)
return
}
}
JSONResponse(w, http.StatusNotFound, "")
}
Almost the same as the addBook function handler with one main difference. For the book to be updated, it has to exist already (ID must be inside of the booksDB). If it exists, we will update the values of the existing book, otherwise, we will return the 404: Not Found error back.
func deleteBook(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bookId, _ := strconv.Atoi(vars["book_id"])
for i, book := range booksDB {
if book.Id == bookId {
booksDB = removeBook(booksDB, i)
_ = json.NewEncoder(w).Encode(book)
return
}
}
JSONResponse(w, http.StatusNotFound, "")
}
After we get the integer value of the book_id variable, we iterate over the booksDB to find the book that the user wants to delete. If it exists, we make use of our helper removeBook function to remove the book from the Book struct slice. If it does not exist, we will return the 404: Not Found error back.
Now that our API is finished, let's give it a run. Execute this in your terminal:
go run main.go
Fire up your favorite HTTP client (Insomnia, Postman, ...) and try out some of the endpoints we created. Feel free to play around with your newly created Go REST API.
The final code can be found here.
Like always, thanks for reading :)