Building an API with Gin Framework in Go

This tutorial aims to help the developers looking to get started with creating Robust REST APIs in Go using Gin framework. By the end of this guide, you'll have a fully functional API that can handle basic CRUD operations for a bookstore, without the integrating with a database.

Let's jump straight into code!

Setting Up Your Project

First, let's set up the directory structure for our project. Create a new directory for your project and set up the following structure:

your-project/
├── cmd/
│   └── main.go
├── internal/
│   ├── handler/
│   │   └── book_handler.go
│   └── model/
│       └── model.go

Ensure that you have setup Go development environment on your machine. If not, then I highly recommend you to read Set up GO Development Environment.

Implementing the Main Server

In the cmd/ directory, create a file named main.go, where we set up a simple HTTP server using the Gin web framework. It handles requests related to book resources (like creating, reading, updating, and deleting books) through a series of API endpoints under /v1/books. The server runs on port 8080 and defines specific routes for each book-related operation.

The program also includes a graceful shutdown feature to safely stop the server when it receives an interrupt signal (like when you press Ctrl+C in the terminal). When the server starts, it listens for incoming HTTP requests. If an interrupt signal is detected, the program waits up to 5 seconds for any ongoing requests to complete, then shuts down. This ensures users don’t experience abrupt disconnections or lose data mid-request.


package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github/dev-toolkit-go/rest-api-gin-framework/internal/handler"

	"github.com/gin-gonic/gin"
)

func main() {
	router := setupRouter()
	srv := &http.Server{Addr: ":8080", Handler: router}

	go func() {
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("listen: %s\n", err)
		}
	}()

	waitForShutdown(srv)
	log.Println("Server exiting")
}

func setupRouter() *gin.Engine {
	bookHandler := handler.NewBookHandler()
	router := gin.Default()

	v1 := router.Group("/api/v1")
	{
		v1.POST("/books", bookHandler.CreateBook)
		v1.GET("/books/:id", bookHandler.GetBook)
		v1.PUT("/books/:id", bookHandler.UpdateBook)
		v1.DELETE("/books/:id", bookHandler.DeleteBook)
		v1.GET("/books", bookHandler.ListBooks)
		v1.GET("/books/top-rated", bookHandler.GetTopRatedBooks)
	}
	return router
}

func waitForShutdown(srv *http.Server) {
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	log.Println("Shutting down server...")

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal("Server forced to shutdown:", err)
	}
}

Creating the Handlers

In the internal/handler/ directory, create a file named book_handler.go. This file will contain the handlers for our bookstore API. These handlers will process HTTP requests and return mock responses.

  1. CreateBook: Handles the creation of a new book by binding JSON input and returning a mock created book response.
  2. GetBook: Retrieves a specific book based on its ID, returning a mock book response.
  3. UpdateBook: Updates the details of an existing book identified by its ID, using the provided JSON data and returning a mock updated book response.
  4. DeleteBook: Deletes a book by its ID and returns a no content response.
  5. ListBooks: Lists all books with a mock response containing an array of sample books.
  6. GetTopRatedBooks: Retrieves a list of top-rated books, returning a mock response with sample top-rated books.
package handler

import (
	"net/http"
	"strconv"

	"github/dev-toolkit-go/rest-api-gin-framework/internal/model"

	"github.com/gin-gonic/gin"
)

type BookHandler struct{}

func NewBookHandler() *BookHandler {
	return &BookHandler{}
}

func (h *BookHandler) CreateBook(c *gin.Context) {
	var book model.Book
	if err := c.ShouldBindJSON(&book); err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{Message: err.Error()})
		return
	}

	createdBook := model.BookResponse {
		ID:     1,
		Title:  book.Title,
		Author: book.Author,
	}

	c.JSON(http.StatusCreated, createdBook)
}

func (h *BookHandler) GetBook(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 32)
	if err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{Message: "Invalid book ID"})
		return
	}

	book := model.BookResponse {
		ID:     uint(id),
		Title:  "Sample Book",
		Author: "Author Name",
	}

	c.JSON(http.StatusOK, book)
}

func (h *BookHandler) UpdateBook(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 32)
	if err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{Message: "Invalid book ID"})
		return
	}

	var book model.Book
	if err := c.ShouldBindJSON(&book); err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{Message: err.Error()})
		return
	}

	updatedBook := model.BookResponse {
		ID:     uint(id),
		Title:  book.Title,
		Author: book.Author,
	}

	c.JSON(http.StatusOK, updatedBook)
}

func (h *BookHandler) DeleteBook(c *gin.Context) {
	_, err := strconv.ParseUint(c.Param("id"), 10, 32)
	if err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{Message: "Invalid book ID"})
		return
	}

	c.JSON(http.StatusNoContent, nil)
}

func (h *BookHandler) ListBooks(c *gin.Context) {
	books := []model.BookResponse {
		{ID: 1, Title: "Sample Book 1", Author: "Author 1"},
		{ID: 2, Title: "Sample Book 2", Author: "Author 2"},
	}

	c.JSON(http.StatusOK, books)
}

func (h *BookHandler) GetTopRatedBooks(c *gin.Context) {
	// Mock response
	topRatedBooks := []model.BookResponse {
		{ID: 1, Title: "Top Rated Book 1", Author: "Author 1"},
		{ID: 2, Title: "Top Rated Book 2", Author: "Author 2"},
	}

	c.JSON(http.StatusOK, topRatedBooks)
}

type ErrorResponse struct {
	Message string `json:"message"`
}

Defining Data Model

Next, define the data model in the internal/model/ directory. Create a file named model.go:

package model

type Book struct {
	Title  string `json:"title"`
	Author string `json:"author"`
}

type BookResponse struct {
	ID     uint   `json:"id"`
	Title  string `json:"title"`
	Author string `json:"author"`
}

Running Server and Testing Your API

Run the HTTP server, by navigating to /cmd directory and

go run main.go

You can test your API using tools like Postman or curl. Here are some sample curl commands to test each endpoint:

Create a book:

curl -X POST http://localhost:8080/v1/books -H "Content-Type: application/json" -d '{"title":"New Book","author":"Author Name"}'

Get a book:

curl http://localhost:8080/v1/books/1

List all books:

curl http://localhost:8080/v1/books

Update a book:

curl -X PUT http://localhost:8080/v1/books/1 -H "Content-Type: application/json" -d '{"title":"Updated Book","author":"Updated Author"}'

Delete a book:

curl -X DELETE http://localhost:8080/v1/books/1
Building an API with Gin Framework in Go
API Server Logs
REST API client curl logs
REST API client curl logs

Conclusion

In this tutorial, we walked through the process of building a simple bookstore API with Gin framework in Go. We set up the project structure, created handlers to manage book-related operations, defined data transfer objects, and implemented the main server. This guide serves as a starting point for building more complex and feature-rich APIs using the Gin framework. Happy coding!

© 2024 Solution Toolkit . All rights reserved.