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:
- Retrieve all books in the store:
- Retrieve a particular book from the store using the book Id
- 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
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
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
You can see that a file, go.mod
is created. Quickly view the content with or open from the editor:
cat go.mod
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
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},
}
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 .
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)
}
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)
}
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"})
}
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")
}
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 .
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
Or use Postman or any API client of choice.
Do the same for the other endpoints.
curl http://localhost:8080/books/2
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}'
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)