In this tutorial, we'll build a real-world RESTful API using the Gin web framework in Go, connected to a PostgreSQL database. We'll structure our application by separating models and services, following best practices to create maintainable and scalable code.
Introduction
We'll create a simple API to manage a list of books, performing CRUD (Create, Read, Update, Delete) operations. The API will connect to a PostgreSQL database, and we'll separate concerns by organizing our code into models and services.
Prerequisites
Go installed (Download Go)
PostgreSQL installed (Download PostgreSQL)
Basic knowledge of Go programming
A code editor (e.g., VSCode, GoLand)
Postman or curl for testing the API
Project Setup
First, create a new directory for your project:
mkdir gin-bookstore
cd gin-bookstore
Initialize a new Go module:
go mod init github.com/yourusername/gin-bookstore
Replace github.com/yourusername/gin-bookstore
with your actual GitHub username or module path.
Installing Dependencies
We'll need the following dependencies:
Gin: Web framework
GORM: ORM library for Go
pq: PostgreSQL driver for Go
Install them using the following commands:
go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
Connecting to PostgreSQL
Setting Up the Database
Before we start coding, let's set up the PostgreSQL database.
- Create a database named
bookstore
.
CREATE DATABASE bookstore;
- Create a
books
table.
Connect to the bookstore
database and run:
CREATE TABLE books (
id SERIAL PRIMARY KEY,
title VARCHAR(100),
author VARCHAR(100),
description TEXT
);
Configuring the Database Connection
Create a new file named database/database.go
:
mkdir database
touch database/database.go
Add the following code to database/database.go
:
package database
import (
"fmt"
"log"
"os"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var DB *gorm.DB
func Connect() {
var err error
dsn := "host=localhost user=yourusername password=yourpassword dbname=bookstore port=5432 sslmode=disable TimeZone=Asia/Shanghai"
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
fmt.Println("Database connection established")
}
Replace yourusername
and yourpassword
with your PostgreSQL username and password.
Creating Models
Create a new directory for models:
mkdir models
touch models/book.go
In models/book.go
, define the Book
struct:
package models
type Book struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title"`
Author string `json:"author"`
Description string `json:"description"`
}
Setting Up Services
Create a directory for services:
mkdir services
touch services/book_service.go
In services/book_service.go
, we'll implement functions to interact with the database.
package services
import (
"github.com/yourusername/gin-bookstore/database"
"github.com/yourusername/gin-bookstore/models"
)
func GetAllBooks(books *[]models.Book) error {
if err := database.DB.Find(books).Error; err != nil {
return err
}
return nil
}
func GetBookByID(book *models.Book, id string) error {
if err := database.DB.Where("id = ?", id).First(book).Error; err != nil {
return err
}
return nil
}
func CreateBook(book *models.Book) error {
if err := database.DB.Create(book).Error; err != nil {
return err
}
return nil
}
func UpdateBook(book *models.Book, id string) error {
if err := database.DB.Model(book).Where("id = ?", id).Updates(book).Error; err != nil {
return err
}
return nil
}
func DeleteBook(book *models.Book, id string) error {
if err := database.DB.Where("id = ?", id).Delete(book).Error; err != nil {
return err
}
return nil
}
Note: Replace github.com/yourusername/gin-bookstore
with your module path.
Implementing API Endpoints
Setting Up the Router
In main.go
, set up the router and connect to the database.
package main
import (
"github.com/gin-gonic/gin"
"github.com/yourusername/gin-bookstore/controllers"
"github.com/yourusername/gin-bookstore/database"
)
func main() {
router := gin.Default()
database.Connect()
router.GET("/books", controllers.GetBooks)
router.GET("/books/:id", controllers.GetBook)
router.POST("/books", controllers.CreateBook)
router.PUT("/books/:id", controllers.UpdateBook)
router.DELETE("/books/:id", controllers.DeleteBook)
router.Run(":8080")
}
Creating Controllers
Create a directory for controllers:
mkdir controllers
touch controllers/book_controller.go
In controllers/book_controller.go
, implement the handler functions:
package controllers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yourusername/gin-bookstore/models"
"github.com/yourusername/gin-bookstore/services"
)
func GetBooks(c *gin.Context) {
var books []models.Book
err := services.GetAllBooks(&books)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, books)
}
func GetBook(c *gin.Context) {
id := c.Param("id")
var book models.Book
err := services.GetBookByID(&book, id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Book not found"})
return
}
c.JSON(http.StatusOK, book)
}
func CreateBook(c *gin.Context) {
var book models.Book
if err := c.ShouldBindJSON(&book); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err := services.CreateBook(&book)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, book)
}
func UpdateBook(c *gin.Context) {
id := c.Param("id")
var book models.Book
if err := c.ShouldBindJSON(&book); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err := services.UpdateBook(&book, id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, book)
}
func DeleteBook(c *gin.Context) {
id := c.Param("id")
var book models.Book
err := services.DeleteBook(&book, id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Book deleted"})
}
Auto-Migrating Models
Modify database/database.go
to auto-migrate the Book
model:
func Connect() {
var err error
dsn := "host=localhost user=yourusername password=yourpassword dbname=bookstore port=5432 sslmode=disable TimeZone=Asia/Shanghai"
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
fmt.Println("Database connection established")
// Auto-migrate the Book model
DB.AutoMigrate(&models.Book{})
}
Don't forget to import the models
package:
import (
// other imports
"github.com/yourusername/gin-bookstore/models"
)
Bonus: Seeding the Database with Initial Data
Seeding your database is an excellent way to populate it with initial data which you can use for development and testing. This ensures that you have a predictable and controlled data set that mimics real-world data.
Step 1: Create the Seed File
Create a new file called seeds.go
in the database
directory:
touch database/seeds.go
In seeds.go
, define a function to seed your books:
package database
import (
"github.com/yourusername/gin-bookstore/models"
"gorm.io/gorm"
)
func SeedBooks(db *gorm.DB) error {
books := []models.Book{
{Title: "1984", Author: "George Orwell", Description: "Dystopian novel about the future."},
{Title: "The Great Gatsby", Author: "F. Scott Fitzgerald", Description: "Narrative on the American dreams."},
// more books
}
for _, book := range books {
err := db.FirstOrCreate(&book, models.Book{Title: book.Title}).Error
if err != nil {
return err
}
}
return nil
}
Step 2: Call the Seed Function
Modify database.go
to include a call to SeedBooks
right after setting up the database connection and running migrations:
func Connect() {
// Existing database connection setup
DB, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
fmt.Println("Database connection established")
DB.AutoMigrate(&models.Book{})
// Seed the database
if err := SeedBooks(DB); err != nil {
log.Fatal("Failed to seed books:", err)
}
}
Step 3: Test Your Seeding
To ensure your seeding is working as expected, start your application and use a PostgreSQL client to check if the books have been added:
SELECT * FROM books;
This will display all books that were inserted into the database during the seeding process.
Testing the API
Start the Server
Run the application:
go run main.go
Using Postman or curl
Create a New Book
Endpoint:
POST http://localhost:8080/books
Body:
{
"title": "The Hitchhiker's Guide to the Galaxy",
"author": "Douglas Adams",
"description": "A science fiction comedy series."
}
Get All Books
- Endpoint:
GET http://localhost:8080/books
Get a Book by ID
- Endpoint:
GET http://localhost:8080/books/1
Update a Book
Endpoint:
PUT http://localhost:8080/books/1
Body:
{
"title": "The Hitchhiker's Guide to the Galaxy (Updated)",
"author": "Douglas Adams",
"description": "An updated description."
}
Delete a Book
- Endpoint:
DELETE http://localhost:8080/books/1
Conclusion
You've successfully built a RESTful API in Go using Gin and PostgreSQL, complete with separated models and services. This structure is scalable and maintainable, following industry best practices.
Next Steps
Implement Validation: Use packages like
go-playground/validator
for input validation.Add Authentication: Implement JWT or OAuth2 for secure API endpoints.
Handle Errors Gracefully: Create custom error handling middleware.
Dockerize the Application: Containerize your app for easy deployment.