Go language book management RESTful API development practice

Go (Golang) is a relatively new programming language that has recently gained popularity.

It's small and stable, easy to use and learn, fast, compiled (native code), and used heavily in cloud tools and services (Docker, Kubernetes...).

Considering all the benefits it offers, there's no reason not to give it a try.

In this tutorial, we will build a simple book store REST API.

Programmer's Treasure Store : https://github.com/Jackpopc/CS-Books-Store

1. Preparations

Before we start, we need to do some preparations in advance:

  • browser

  • gorilla/handlers

  • gorilla/mux

With these preparations in place, we can start our Go journey!

2. Application structure

You should now have Go installed and ready to go.

Open your favorite IDE (Visual Studio Code, GoLand, ...) and create a new project.

As I mentioned earlier, the idea was to build a simple REST API for book store management by using Mux.

Once you've created your blank project, create the following structure in it:

├── main.go
└── src
    ├── app.go
    ├── data.go
    ├── handlers.go
    ├── helpers.go
    └── middlewares.go

Go toolkits and modules

Let's start by looking at Go modules and packages, if you're familiar with Python, you might have an idea of ​​these things because they operate similarly.

The best way to describe a Go package is that it is a collection of source files in the same directory, compiled into a reusable unit.

This means that all files with a similar purpose should be placed in one package.

Following our structure above, srcis one of our packages.

A Go module is a collection of Go packages and their dependencies, which means that a module can consist of multiple packages.

For ease of understanding, you can think of our entire application as a Go module.

Let's execute this command in the project root directory to create our module.

go mod init bookstore

You should see a new file in your root directory called go.mod.

3. Build the API

Now it's time to start building our application.

Open your main.gofile and insert the following code in it.

package main
import"bookstore/src"
func main() {
    src.Start()
}

We declare our main Go package ( package main) and import our srcpackage with the module bookstore's prefix.

In the function main(), we will run srcthe Start()function of the package.

main.goThis is the sole responsibility of our entry file ( ) - to start the API.

routes and handlers

Now we need to create our API router ( Mux) and configure it by creating some endpoints and their handlers.

Open in your src package app.goand insert the following code in 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, our declaration app.gois part of the src package, which contains the functions we main.gouse in the file Start().

We also import two external modules, which we need to depend on in our muxprogram handlers.

Execute the following command in your terminal:

go get github.com/gorilla/handlers
go get github.com/gorilla/mux

Your go.modfile should also be synced, now it should look like this:

module bookstore
go1.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 declare a new Muxrouter variable that will be responsible for routing and handling requests for the entire API.

Then, we tell Muxthat we are going to use a middleware that will execute the following line on every request that comes to our API:

router.Use(commonMiddleware)

More on middleware later.

Continuing to analyze our code, we can see where we create endpoints along with handlers (callback functions) and some primitive validations like:

router.HandleFunc("/book/{book_id:[0-9]+}", updateBook).Methods(http.MethodPut)

This endpoint starts as soon as the user hits our server /book/123with a method (or any other number) on the path .PUT

It will then pass the request to the updateBookhandler function for further processing.

book_idThe variable must be a number because we specify a simple validation after the variable name declaration.

Finally, we'll run our server on a specific host and port combination and have it log everything to our terminal.

middleware

We all know that REST APIs mostly use JSON when accepting requests and returning responses.

Content-TypeThis is communicated to our browser/HTTP client using headers.

Since our API will only use JSON-represented data, we can use a middleware to ensure our content-type is always set to JSON.

As mentioned, app.gothe Start()method contains this line:

router.Use(commonMiddleware)

Let's open our middlewares.gofile and create the required functions:

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 Start()any of the endpoints we registered with the Mux router in the function, the middleware will intercept the request and add commonMiddlewarethe two headers we specified in the function.

It will then pass the modified request further to the handler function of the requested endpoint or another middleware.

static data

Since we won't be using any data storage services (databases, caches...), we need to have some kind of static data.

Also, we will create a data type for the custom response, which I will explain later.

Open srcthe package and data.goput the following contents in 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 book in our API.

I also created json tag which will translate the field name to JSON representation if the data type will be passed in JSON. Also, a primitive book storage system (in memory) and some initial book data (booksDB) are created.

Add this code below the table 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 our API will return, more on this later.

Auxiliary tool

We will need some helper tools to get the most out of our API. For example, we will need to check if a book with a given ID exists (add a new book, modify an existing book), need to delete a book with a given ID (delete a book), need to return a self for a given HTTP status code Defined JSON response.

Open the src package and helpers.goinsert the following in it:

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,
        Post: post,
        Description: desc,
    }
    _ = json.NewEncoder(w).Encode(r)
}

removeBookThe function iterates Bookand if it's not the last element of the fragment, it will move it to the end of the fragment and return a new fragment without it (avoiding the last element).

checkDuplicateBookIdThe function will return a bool value (true or false) depending on whether the given id exists in Book.

JSONResponseCustomResponseThe function is responsible for using the sum we created earlier responseCodes. It will return a JSON representation of the CustomResponse with the status code and message that responseCodes will provide.

This way we will avoid having different messages in our API for the same HTTP status code.

handler

Now comes the final step, putting the endpoint handlers together.

Open yours handlers.goand let's put some code in it:

package src
import (
    "encoding/json"
    "github.com/gorilla/mux"
    "net/http"
    "strconv"
)

Get a single book

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, "")
}

We get the passed variable from the Mux router and convert it from a string to an int value. Then, traverse our booksDB looking for matching book IDs. If it exists, return it - if not, we return 404: Not Foundan error.

Get all books

func getAllBooks(w http.ResponseWriter, r *http.Request) {
    _ = json.NewEncoder(w).Encode(booksDB)
}

Is not it simple? Convert booksDB to JSON and return it to the user.

Add a new book

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 is triggered on the POST method, the user must provide JSON data in the request body that conforms to the Book structure.

{
    "id": 999,
    "title": "SomeTitle",
    "author": "SomeAuthor",
    "genre": "SomeGenre"
}

Once we decode and validate the JSON body against our book structure (which we will return if it fails 400: Bad Request error), we need to check if a book with the same ID already exists. If so, we will return 409: Conflict error back. Instead, our booksDB will be appended with the user-provided books and its JSON representation will be returned to the user.

Update existing books

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 major difference.

To update the book, it must already exist (ID must be in booksDB).

If it exists, we will update the value of the existing book, otherwise, we will return 404: Not Foundan error.

delete existing books

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 through the booksDB to find the book the user wants to delete.

If it exists, we use our helper function removeBookto remove the book from the Book structure slice. If it doesn't exist, we will return 404: Not Foundan error.

4. Run and test the API

Now that our API is complete, let's run it and execute this program in your terminal:

go run main.go

Fire up your favorite HTTP client (Insomnia, Postman, ...) and try out some of the interfaces we've created

Guess you like

Origin blog.csdn.net/jakpopc/article/details/123024018