DEV Community

Cover image for A Beginner-friendly Approach to Developing a REST API with Go and Gin
Gospel Lekia
Gospel Lekia

Posted on • Updated on

A Beginner-friendly Approach to Developing a REST API with Go and Gin

We learn to learn again (until we come to an "Aha!" moment if we're lucky enough). Let's get to the heart of it.

Definitons

Go is a statically typed, compiled high-level programming language designed at Google. It's a modern programming language well-positioned for the evolving technology needs.

Gin is a web framework written in Go that simplifies building web applications.

Fully functional REST APIs can be developed with Go native packages. However, there are tools created to increase efficiency and reduce development time, and very likely you will be working more with frameworks.

Prerequisite

  • Basic understanding of REST
  • Introductory knowledge of Go
  • Introductory knowledge of Gin (optional)

Tools

  • A computer, laptop or anything similar.
  • Go install and set up. Installation instruction
  • Any code editor or IDE of choice e.g. Vscode
  • A command terminal, cmd or PowerShell (windows)
  • Curl or any API client e.g Postman
  • Git for version control (optional)
  • A repository to store your code (optional)

In this tutorial:

  • Design API endpoints
  • Setup project structure
  • Create the data store
  • Write the corresponding endpoint handlers:
    • Handler to return all books
    • Handler to add a new book to the data store
    • Handler to return a specific book
  • Associate Handles to Request Endpoints
  • Conclusion

What We will be building

A simple bookstore service where books can be added and retrieved.

Design API endpoints

API design typically starts with writing out the endpoints. We'll have endpoints to:

  1. Retrieve all books in the store:
  2. Retrieve a particular book from the store using the book Id
  3. Add a book to the store.

/books

  • GET - return a list of all books as JSON.
  • POST - add a new book resource from a JSON request.

/books/:id

  • GET - retrieve a book by using the ID, returned as JSON.

For simplicity, there are things we did not consider in designing these API endpoints.
In this tutorial, we'll not consider API versioning, middleware, authentication, database etc. Those will be coming in future tutorials as a continuation of this.

Set up Project Structure

Using any editor of choice create a directory to hold your project, and name the directory bookstore.
Or from your terminal:

cd ~
mkdir bookstore
cd bookstore
Enter fullscreen mode Exit fullscreen mode

Every programming language has a way of managing packages and dependencies. In Go, dependency is managed using Go module.

So let's create a module to manage/track dependencies similar to npm init using our terminal.

go mod init github.com/yigaue/bookstore
go: creating new go.mod: module github.com/yigaue/bookstore
Enter fullscreen mode Exit fullscreen mode

You can use any path of your choice. It's perfectly fine. However, it's common to see developers append the project name to their GitHub username like above, in that way, it's easy to publish their code as a package and reuse it in other code.
Without using Github we can simply do:

go mod init project/bookstore
go: creating new go.mod: module project/bookstore
Enter fullscreen mode Exit fullscreen mode

You can see that a file, go.mod is created. Quickly view the content with or open from the editor:

cat go.mod
Enter fullscreen mode Exit fullscreen mode

This file contains the module path: the import path prefix for all packages within the module.

Create Data Store

For simplicity, we'll store our data in memory. A typical API will store data in a database. For this, we'll use struct and slice. Do not worry if slice and struct are not familiar, you can read more about them.
The data in memory is not persistent, which means the data is lost every time we stop and start our local server.

Create a main.go file in the project directory.

touch main.go
Enter fullscreen mode Exit fullscreen mode

Add the following code to the main.go file.

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

// book struct defines a book similar to an object
type book struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Author string  `json:"author"`
    Price  float64 `json:"price"`
}


var books = []book {
    {ID: "1", Title: "A Day in the Life of Abed Salama", Author: "Nathan Thrall", Price: 56.99},
    {ID: "2", Title: "King: A life", Author: "Jonathan Eig", Price: 56.99},
    {ID: "3", Title: "Where we go from here", Author: "Ken Wiwa", Price: 17.99},
    {ID: "4", Title: "Buiding a dream server", Author: "Lekia Yiga", Price: 39.99},
    {ID: "5", Title: "Clean Code ", Author: "Robert C Martin", Price: 39.99},
}
Enter fullscreen mode Exit fullscreen mode

Explanation: The first line is our package declaration. package main is required for an executable standalone program. A package is similar to a namespace. Run the command to fetch all dependencies in the import.

go get .
Enter fullscreen mode Exit fullscreen mode

All dependencies in the import statement will be fetched and a sum.go file will be created to lock our dependencies, similar to package-lock.json.

Below the package declaration is the import of needed dependencies.
The book struct defines the structure of a book like a book object. When a book is created it follows the blueprint of the book struct. A book will have a title, author, ISBN, and price amongst others. Note that we have added a struct tag like json:"title". This will ensure that the JSON key returned is formatted as specified and does not use the struct keys like "Title".

Beneath the book definition is the book store: a slice of books. Five books have been added to the collection as samples. A slice in Go is similar to an array but more flexible. We can easily manipulate the slice, adding and removing items.

Write the Corresponding Endpoint Handlers

What we want to do here is map the request path to the appropriate handler(a portion of code that executes the logic associated with that request path).

Handler to return all books

GET: /books - call the getBooks handler when a user visits the /books endpoint. Add the function below to the existing code. The function takes the gin.Context as a parameter. Recall the gin package is imported at the beginning of this file.

  func getBooks(c *gin.Context) {
     c.IndentedJSON(http.StatusOK, books)
  }
Enter fullscreen mode Exit fullscreen mode

IndentedJSON serializes the book struct to JSON and the response is returned to the client. The response code http.StatusOk is the first parameter of IndentedJSON, a constant with a value of 200.

Handler to add a new book to the data store

When a post request is made to /books/ we want to add a new book. Let's implement the handler to handle this logic.

Add the following function to the existing code.

  func postBook(c *gin.Context) {
    var newBook book

    if err := c.BindJSON(&newBook); err != nil {
        return
    }

    books = append(books, newBook)
    c.IndentedJSON(http.StatusCreated, newBook)
  }
Enter fullscreen mode Exit fullscreen mode

The postBook function takes a gin.Context parameter. A newBook variable is declared in the function which is of type book with underlining type as struct (declared above). The request body JSON is bound to the newBook variable with BindJSON(.... If an error occurs, we exit. append(books, newBook) appends the newBook variable to the existing books slice(data store). Finally, c.IndentedJSON(http.StatusCreated, newBook) returns the resulting book JSON to the client making the request.

Handler to return a specific book

/books/:id endpoint GET method retrieves a book. The handler function is thus:

  func getBook(c *gin.Context) {
    id := c.Param("id")
    for _, book := range books {
        if book.ID == id {
            c.IndentedJSON(http.StatusOK, book)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": 
 "book not found"})
 }
Enter fullscreen mode Exit fullscreen mode

Let's go through what the function is doing.

gin.Context is passed as a parameter to the getBook function. c.Param("id") retrieves the URL path variable, "id". We loop over the book structs in the slice to find a struct whose ID field matches the id parameter from the URL path. If there is a match we serialize the matching book struct to JSON using c.IndentedJSON... and return it with an http.statusOk response. If there is no match StatusNotFound response is returned.

Associate Handles to Request Endpoints

Now that the Handlers and the endpoint are defined, Let's map the Handlers to the request endpoints.

create a function name main like so.

func main() {
    router := gin.Default()
    router.GET("/books", getBooks)
    router.GET("/books/:id", getBook)
    router.POST("/books", postBook)
    router.Run("localhost:8080")
}
Enter fullscreen mode Exit fullscreen mode

In the main function we initialize a gin router using gin.Default(), then associate individual endpoints to the corresponding Handlers we already defined.

Format your code nicely using go fmt from the terminal.
Start the server and run the code with the command:

go run .
Enter fullscreen mode Exit fullscreen mode

You should see a result similar to GIN-debug] Listening and serving HTTP on localhost:8080 in your terminal.
Open a new terminal and run the command:

curl http://localhost:8080/books
Enter fullscreen mode Exit fullscreen mode

Or use Postman or any API client of choice.

Result:
Get all books resource image result

Do the same for the other endpoints.

curl http://localhost:8080/books/2
Enter fullscreen mode Exit fullscreen mode

Result:
Get a book image image result

curl http://localhost:8080/books \
    --include \
    --header "Content-Type: application/json" \
    --request "POST" \
    --data '{"id": "6", "title": "Things fall apart","author": "Chinua Achebe", "price": 30.90}'
Enter fullscreen mode Exit fullscreen mode

Result:
Add a new book resource image result

After adding a book you can verify with the /books endpoint that the new book was added.

Conclusion

In this tutorial, we were able to show a simple implementation of REST API using the Gin framework. Certain considerations were not taken in the implementation. You may have noticed that our data store (Book slice) can hold many non-unique records. That's to say we can have books with the same IDs.
In the coming tutorial, we'll swap our in-memory store with an actual database.

You can find the full code here on GitHub.
Feel free to create an issue or raise a pull request if you find an issue.

Top comments (0)