As a web developer, building APIs or frontend applications that interact with other APIs is one essential project you can undertake to improve your skills or add to your portfolio to attract potential employers.

This article will teach you how to create a CRUD RESTful API in a Golang environment that runs on a Gin Gonic server and uses a PostgreSQL database.

We’ll also discuss how you can build models, connect to the running SQL database server, and run database migrations with the GORM library.

Build Golang, GORM, PostgreSQL RESTful API Series:

  1. How to Setup Golang GORM RESTful API Project with Postgres
  2. API with Golang + GORM + PostgreSQL: Access & Refresh Tokens
  3. Golang and GORM – User Registration and Email Verification
  4. Forgot/Reset Passwords in Golang with HTML Email
  5. Build a RESTful CRUD API with Golang
Build a RESTful CRUD API with Golang

Prerequisites

Before continuing with this tutorial, you will need the following:

  • VS Code as the IDE for this project. I mostly use VS Code as my default IDE for building all my projects because it’s lightweight and supports almost every programming language.
    However, feel free to use any IDE you are comfortable with.
  • Familiarity with Golang, pgAdmin, and how ORMs work will be beneficial.
  • Have a working Golang development environment. Click here to download the latest version of Golang.
  • Have Docker and Docker-compose installed on your system.

What is a REST API

A REST or RESTful API is an architectural style computer systems use to exchange information securely over the internet. Similar to other architectural styles, REST (REpresentational State Transfer) also has its guiding principles, set of standards, and constraints.

This set of standards must be implemented if a service interface needs to be referred to as RESTful.

RESTful APIs are stateless, cacheable, scalable, consume less bandwidth, and are generally easier to use compared to prescribed protocols like SOAP (Simple Object Access Protocol).

What is a CRUD API

CRUD is an acronym that stands for CREATE, READ, UPDATE, and DELETE. This acronym is the standardized use of HTTP Action Verbs to create, read, update, and delete resources.

Usually, programmers create RESTful APIs that utilize HTTP methods to handle basic CRUD operations. For example:

  • Create – Uses the HTTP POST method to add one or more records.
  • Read – Uses the HTTP GET method to retrieve one or more records that match certain criteria.
  • Update – Uses the HTTP PUT or PATCH methods to update a record.
  • Delete – Uses the HTTP DELETE method to remove one or more records.

What is GORM

GORM is a fantastic object-relational mapper (ORM) library for Golang that claims to help developers build faster and make fewer errors.

The GORM library is built on top of the Golang database/sql package. That means it only works with relational databases (MySQL, PostgreSQL, SQLite, etc). Like other ORMs, GORM also provides a lot of tools to help developers interact with databases with ease.

What is Gin Gonic

According to the official Gin Gonic Documentation, Gin Gonic is a high-performant web framework written in Go (Golang). It satisfies Martini-like API standards and claims to be 40 times faster than Martini.

If you already have some expertise with web frameworks like Express, Fastify, FastAPI, and more then getting up and running with Gin will be easy since they all implement similar interfaces.

If you landed on this article from a Google search then you need to catch up by following the Golang setup article before continuing with this tutorial.

Create the Database Models with GORM

GORM offers an extremely powerful and flexible system for managing the connection between Golang structs and their corresponding SQL representations in the database.

A GORM model is simply a struct with basic Golang types, pointers/alias of the types, or custom types implementing Scanner and Valuer interfaces.

By default, GORM prefers convention over configuration and if the recommended convention doesn’t match your requirements, GORM gives you the freedom to configure them.

When creating a GORM model, you are encouraged to use PascalCase syntax for the model and column names since GORM will pluralize the table names and use snake_case notation for the columns.

Also, struct tags are optional when declaring the GORM models. Tags are case insensitive, however, camelCase notation is preferred.

Lastly, GORM allows you to use the native database types in the gorm:"" tag annotation.

models/post.model.go


package models

import (
	"time"

	"github.com/google/uuid"
)

type Post struct {
	ID        uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primary_key" json:"id,omitempty"`
	Title     string    `gorm:"uniqueIndex;not null" json:"title,omitempty"`
	Content   string    `gorm:"not null" json:"content,omitempty"`
	Image     string    `gorm:"not null" json:"image,omitempty"`
	User      uuid.UUID `gorm:"not null" json:"user,omitempty"`
	CreatedAt time.Time `gorm:"not null" json:"created_at,omitempty"`
	UpdatedAt time.Time `gorm:"not null" json:"updated_at,omitempty"`
}

type CreatePostRequest struct {
	Title     string    `json:"title"  binding:"required"`
	Content   string    `json:"content" binding:"required"`
	Image     string    `json:"image" binding:"required"`
	User      string    `json:"user,omitempty"`
	CreatedAt time.Time `json:"created_at,omitempty"`
	UpdatedAt time.Time `json:"updated_at,omitempty"`
}

type UpdatePost struct {
	Title     string    `json:"title,omitempty"`
	Content   string    `json:"content,omitempty"`
	Image     string    `json:"image,omitempty"`
	User      string    `json:"user,omitempty"`
	CreateAt  time.Time `json:"created_at,omitempty"`
	UpdatedAt time.Time `json:"updated_at,omitempty"`
}


Let’s evaluate the structs we defined above. First, we created a Post struct that will be transformed into the corresponding SQL table in the database.

In the ID column, we used the uuid_generate_v4() function as the default value. By default, Postgres supports the UUID data type but since we used the uuid_generate_v4() function as the default value, we need to manually install the UUID OSSP extension for it to work.

Follow the Golang setup article to learn how to install the UUID OSSP module.

Then, we created two additional structs (CreatePostRequest and UpdatePost) that will be used by the Gin Gonic framework to validate the request body.

Generating the SQL Table with GORM

Now that we have the GORM model defined, it’s time to run the database migration to push the schema to the database.

Database schema migration is a technique used to track the incremental and reversible changes done on a relational database schema.

GORM provides us with an AutoMigrate method that we have to evoke to automatically migrate the schema to the database.

migrate/migrate.go


package main

import (
	"fmt"
	"log"

	"github.com/wpcodevo/golang-gorm-postgres/initializers"
	"github.com/wpcodevo/golang-gorm-postgres/models"
)

func init() {
	config, err := initializers.LoadConfig(".")
	if err != nil {
		log.Fatal("? Could not load environment variables", err)
	}

	initializers.ConnectDB(&config)
}

func main() {
	initializers.DB.AutoMigrate(&models.User{}, &models.Post{})
	fmt.Println("? Migration complete")
}


In the above, we created a Go init function to load the environment variables into memory and create a connection pool to the PostgreSQL database.

Then, we created the Go main function and evoked the GORM AutoMigrate method to migrate the schema to the database.

Open, the integrated terminal in VS Code and run the migration script to push the schema to the database.


go run migrate/migrate.go 

To see the SQL tables added by the GORM AutoMigrate tool, open any PostgreSQL client and sign in with the credentials provided in the app.env file.

In this example, I’m going to use pgAdmin, a popular client for accessing and mutating Postgres databases.

log into the postgres docker container with pgadmin

After logging in, expand the dropdowns and open the query tool on the posts table to see the columns added by GORM.

golang rest api view post record in postgres

Creating CRUD Functions in a RESTful API

The aim of this tutorial is to implement the following CRUD operations on the Golang API:

RESOURCEHTTP METHODROUTEDESCRIPTION
postsGET/api/postsFetch all posts
postsPOST/api/postsAdd a new post
postsGET/api/posts/:postIdRetrieve a single post
postsPUT/api/posts/:postIdUpdate a post
postsDELETE/api/posts/:postIdDelete a post

Each of these five operations will run their corresponding GORM CRUD functions to query and mutate the PostgreSQL database.

Create a Constructor for the CRUD Operations

Before we start creating the route controllers, add the following imports and create a struct to extend the gorm.DB struct. Extending the gorm.DB struct will allow us to have access to all the APIs the GORM library exposes including the database CRUD functions.

controllers/post.controller.go


package controllers

import (
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/wpcodevo/golang-gorm-postgres/models"
	"gorm.io/gorm"
)

type PostController struct {
	DB *gorm.DB
}

func NewPostController(DB *gorm.DB) PostController {
	return PostController{DB}
}

Create Operation Route Handler

In this section, you will create the route handler that will be called to insert the new post into the database.

controllers/post.controller.go


// [...] Create Post Handler
func (pc *PostController) CreatePost(ctx *gin.Context) {
	currentUser := ctx.MustGet("currentUser").(models.User)
	var payload *models.CreatePostRequest

	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadRequest, err.Error())
		return
	}

	now := time.Now()
	newPost := models.Post{
		Title:     payload.Title,
		Content:   payload.Content,
		Image:     payload.Image,
		User:      currentUser.ID,
		CreatedAt: now,
		UpdatedAt: now,
	}

	result := pc.DB.Create(&newPost)
	if result.Error != nil {
		if strings.Contains(result.Error.Error(), "duplicate key") {
			ctx.JSON(http.StatusConflict, gin.H{"status": "fail", "message": "Post with that title already exists"})
			return
		}
		ctx.JSON(http.StatusBadGateway, gin.H{"status": "error", "message": result.Error.Error()})
		return
	}

	ctx.JSON(http.StatusCreated, gin.H{"status": "success", "data": newPost})
}


Quite a lot happening in the above code, let’s break it down:

  • First, we evoked the MustGet() method provided by Gin Gonic to obtain the authenticated user’s credentials from the context object. This step is required because we need to reference the ID of the user creating the post.
  • Then, we called the ShouldBindJSON() method to validate the request body before defining the Post struct arguments.
  • Next, we called the Create() CRUD function provided by GORM to insert the new record into the database.
  • Lastly, we returned a JSON response with the newly-created post to the client.

Update Operation Route Handler

Similar to the CreatePost controller, we will extract the post ID from the request parameters before validating the request body to ensure that the user provides all the required fields.

Next, we will query the database to check if a post with that ID exists and construct the Post arguments.

Then, we will call the Updates() CRUD function provided by GORM to update the record in the database.

controllers/post.controller.go


// [...] Create Post Handler

// [...] Update Post Handler
func (pc *PostController) UpdatePost(ctx *gin.Context) {
	postId := ctx.Param("postId")
	currentUser := ctx.MustGet("currentUser").(models.User)

	var payload *models.UpdatePost
	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadGateway, gin.H{"status": "fail", "message": err.Error()})
		return
	}
	var updatedPost models.Post
	result := pc.DB.First(&updatedPost, "id = ?", postId)
	if result.Error != nil {
		ctx.JSON(http.StatusNotFound, gin.H{"status": "fail", "message": "No post with that title exists"})
		return
	}
	now := time.Now()
	postToUpdate := models.Post{
		Title:     payload.Title,
		Content:   payload.Content,
		Image:     payload.Image,
		User:      currentUser.ID,
		CreatedAt: updatedPost.CreatedAt,
		UpdatedAt: now,
	}

	pc.DB.Model(&updatedPost).Updates(postToUpdate)

	ctx.JSON(http.StatusOK, gin.H{"status": "success", "data": updatedPost})
}


Retrieve a Single Record Route Handler

In this section, you will extract the post ID from the request URL and query the database to see if a post with the provided ID exists before returning the found record to the user.

controllers/post.controller.go


// [...] Create Post Handler

// [...] Update Post Handler

// [...] Get Single Post Handler
func (pc *PostController) FindPostById(ctx *gin.Context) {
	postId := ctx.Param("postId")

	var post models.Post
	result := pc.DB.First(&post, "id = ?", postId)
	if result.Error != nil {
		ctx.JSON(http.StatusNotFound, gin.H{"status": "fail", "message": "No post with that title exists"})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"status": "success", "data": post})
}


Retrieve All Records Route Handler

Here, you will create the controller that will be called to retrieve all the posts in the database. To avoid sending large JSON data to the client, we will add a pagination feature to the API where users can request a paginated list of the posts in the database.

controllers/post.controller.go


// [...] Create Post Handler

// [...] Update Post Handler

// [...] Get Single Post Handler

// [...] Get All Posts Handler
func (pc *PostController) FindPosts(ctx *gin.Context) {
	var page = ctx.DefaultQuery("page", "1")
	var limit = ctx.DefaultQuery("limit", "10")

	intPage, _ := strconv.Atoi(page)
	intLimit, _ := strconv.Atoi(limit)
	offset := (intPage - 1) * intLimit

	var posts []models.Post
	results := pc.DB.Limit(intLimit).Offset(offset).Find(&posts)
	if results.Error != nil {
		ctx.JSON(http.StatusBadGateway, gin.H{"status": "error", "message": results.Error})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"status": "success", "results": len(posts), "data": posts})
}



Delete Operation Route Handler

Finally, let’s create the simplest route handler to remove a post from the database.

controllers/post.controller.go


// [...] Create Post Handler

// [...] Update Post Handler

// [...] Get Single Post Handler

// [...] Get All Posts Handler

// [...] Delete Post Handler
func (pc *PostController) DeletePost(ctx *gin.Context) {
	postId := ctx.Param("postId")

	result := pc.DB.Delete(&models.Post{}, "id = ?", postId)

	if result.Error != nil {
		ctx.JSON(http.StatusNotFound, gin.H{"status": "fail", "message": "No post with that title exists"})
		return
	}

	ctx.JSON(http.StatusNoContent, nil)
}


Complete CRUD Operations

controllers/post.controller.go


package controllers

import (
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/wpcodevo/golang-gorm-postgres/models"
	"gorm.io/gorm"
)

type PostController struct {
	DB *gorm.DB
}

func NewPostController(DB *gorm.DB) PostController {
	return PostController{DB}
}

func (pc *PostController) CreatePost(ctx *gin.Context) {
	currentUser := ctx.MustGet("currentUser").(models.User)
	var payload *models.CreatePostRequest

	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadRequest, err.Error())
		return
	}

	now := time.Now()
	newPost := models.Post{
		Title:     payload.Title,
		Content:   payload.Content,
		Image:     payload.Image,
		User:      currentUser.ID,
		CreatedAt: now,
		UpdatedAt: now,
	}

	result := pc.DB.Create(&newPost)
	if result.Error != nil {
		if strings.Contains(result.Error.Error(), "duplicate key") {
			ctx.JSON(http.StatusConflict, gin.H{"status": "fail", "message": "Post with that title already exists"})
			return
		}
		ctx.JSON(http.StatusBadGateway, gin.H{"status": "error", "message": result.Error.Error()})
		return
	}

	ctx.JSON(http.StatusCreated, gin.H{"status": "success", "data": newPost})
}

func (pc *PostController) UpdatePost(ctx *gin.Context) {
	postId := ctx.Param("postId")
	currentUser := ctx.MustGet("currentUser").(models.User)

	var payload *models.UpdatePost
	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadGateway, gin.H{"status": "fail", "message": err.Error()})
		return
	}
	var updatedPost models.Post
	result := pc.DB.First(&updatedPost, "id = ?", postId)
	if result.Error != nil {
		ctx.JSON(http.StatusNotFound, gin.H{"status": "fail", "message": "No post with that title exists"})
		return
	}
	now := time.Now()
	postToUpdate := models.Post{
		Title:     payload.Title,
		Content:   payload.Content,
		Image:     payload.Image,
		User:      currentUser.ID,
		CreatedAt: updatedPost.CreatedAt,
		UpdatedAt: now,
	}

	pc.DB.Model(&updatedPost).Updates(postToUpdate)

	ctx.JSON(http.StatusOK, gin.H{"status": "success", "data": updatedPost})
}

func (pc *PostController) FindPostById(ctx *gin.Context) {
	postId := ctx.Param("postId")

	var post models.Post
	result := pc.DB.First(&post, "id = ?", postId)
	if result.Error != nil {
		ctx.JSON(http.StatusNotFound, gin.H{"status": "fail", "message": "No post with that title exists"})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"status": "success", "data": post})
}

func (pc *PostController) FindPosts(ctx *gin.Context) {
	var page = ctx.DefaultQuery("page", "1")
	var limit = ctx.DefaultQuery("limit", "10")

	intPage, _ := strconv.Atoi(page)
	intLimit, _ := strconv.Atoi(limit)
	offset := (intPage - 1) * intLimit

	var posts []models.Post
	results := pc.DB.Limit(intLimit).Offset(offset).Find(&posts)
	if results.Error != nil {
		ctx.JSON(http.StatusBadGateway, gin.H{"status": "error", "message": results.Error})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"status": "success", "results": len(posts), "data": posts})
}

func (pc *PostController) DeletePost(ctx *gin.Context) {
	postId := ctx.Param("postId")

	result := pc.DB.Delete(&models.Post{}, "id = ?", postId)

	if result.Error != nil {
		ctx.JSON(http.StatusNotFound, gin.H{"status": "fail", "message": "No post with that title exists"})
		return
	}

	ctx.JSON(http.StatusNoContent, nil)
}


Creating Routes for the CRUD Operations

Now that we have all the controllers defined, let’s create the routes for each controller and add the appropriate HTTP method.

routes/post.routes.go


package routes

import (
	"github.com/gin-gonic/gin"
	"github.com/wpcodevo/golang-gorm-postgres/controllers"
	"github.com/wpcodevo/golang-gorm-postgres/middleware"
)

type PostRouteController struct {
	postController controllers.PostController
}

func NewRoutePostController(postController controllers.PostController) PostRouteController {
	return PostRouteController{postController}
}

func (pc *PostRouteController) PostRoute(rg *gin.RouterGroup) {

	router := rg.Group("posts")
	router.Use(middleware.DeserializeUser())
	router.POST("/", pc.postController.CreatePost)
	router.GET("/", pc.postController.FindPosts)
	router.PUT("/:postId", pc.postController.UpdatePost)
	router.GET("/:postId", pc.postController.FindPostById)
	router.DELETE("/:postId", pc.postController.DeletePost)
}


In the above, we called the DeserializeUser() middleware to validate the user’s identity before allowing them to Create/Read/Update/Delete the records in the database.

Update/Configure the Golang API Server

Here comes the final part. First, initialize the constructor functions of the controllers and routes in the Go init function.

Next, evoke the PostRouteController.PostRoute(router) function to register all the CRUD endpoints.

Note: Remember to configure the Gin server to accept requests from cross-origin domains by installing the CORS package.

main.go


package main

import (
	"log"
	"net/http"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"github.com/wpcodevo/golang-gorm-postgres/controllers"
	"github.com/wpcodevo/golang-gorm-postgres/initializers"
	"github.com/wpcodevo/golang-gorm-postgres/routes"
)

var (
	server              *gin.Engine
	AuthController      controllers.AuthController
	AuthRouteController routes.AuthRouteController

	UserController      controllers.UserController
	UserRouteController routes.UserRouteController

	PostController      controllers.PostController
	PostRouteController routes.PostRouteController
)

func init() {
	config, err := initializers.LoadConfig(".")
	if err != nil {
		log.Fatal("? Could not load environment variables", err)
	}

	initializers.ConnectDB(&config)

	AuthController = controllers.NewAuthController(initializers.DB)
	AuthRouteController = routes.NewAuthRouteController(AuthController)

	UserController = controllers.NewUserController(initializers.DB)
	UserRouteController = routes.NewRouteUserController(UserController)

	PostController = controllers.NewPostController(initializers.DB)
	PostRouteController = routes.NewRoutePostController(PostController)

	server = gin.Default()
}

func main() {
	config, err := initializers.LoadConfig(".")
	if err != nil {
		log.Fatal("? Could not load environment variables", err)
	}

	corsConfig := cors.DefaultConfig()
	corsConfig.AllowOrigins = []string{"http://localhost:8000", config.ClientOrigin}
	corsConfig.AllowCredentials = true

	server.Use(cors.New(corsConfig))

	router := server.Group("/api")
	router.GET("/healthchecker", func(ctx *gin.Context) {
		message := "Welcome to Golang with Gorm and Postgres"
		ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": message})
	})

	AuthRouteController.AuthRoute(router)
	UserRouteController.UserRoute(router)
	PostRouteController.PostRoute(router)
	log.Fatal(server.Run(":" + config.ServerPort))
}


Testing the Golang CRUD API with Postman

To test the Go API, you can use any API testing tool like Postman, Insomnia, or a VS Code extension like Thunder Client to interact with the API.

Click here to import the Postman collection JSON file used in testing this API into your Postman software to make your life easier.

You can follow the How To Make HTTP Requests in VS Code (No Postman) article to learn more about the Thunder Client VS Code extension.

Log into the API

The API is designed in such a way that you need to obtain an access token from the Golang server before you can perform the CRUD operations. This step is required because a post must belong to a user.

Follow the API with Golang + GORM + PostgreSQL: Access & Refresh Tokens article to implement the authentication part of the tutorial.

Now make a POST request with your credentials to the api/auth/register endpoint to create a new account.

Next, provide the credentials and make another POST request to the api/auth/login endpoint to obtain an access token from the Golang server.

golang crud restful api login the user

Creating a New Record

We are now ready to add a new record to the database. Note: After logging into your account, the access token will be sent to Postman as an HTTPOnly cookie. Postman will automatically include it in every subsequent request made to the server.

Alternatively, you can copy and add it to the authorization header as a Bearer token.

With that out of the way, provide the necessary information and make a POST request to the api/posts endpoint to add the new post to the database.

golang crud restful api create operation

Updating the Record

Here, copy the UUID from the newly-created post and add it to the URL parameters. Next, edit the post information and make a PUT request to the api/posts/:postId endpoint to update the post in the database.

golang crud restful api update operation

Request a Single Record

Similar to the PUT operation, copy the UUID of the record you want to request from the Golang API and add it to the URL parameters.

Next, make a GET request to theapi/posts/:postId endpoint to retrieve the post with that ID from the database.

golang crud restful api get single record

Retrieve all Records with Paginated Results

To fetch all the records in the database, make a GET request to the api/posts endpoint.

Also, you can provide the page and limit query parameters to retrieve a paginated list of the records.

golang crud restful api retrieve all records

Delete a Record

To remove a record from the database, make a DELETE request to the api/posts/:postId endpoint with the ID of the post you want to delete.

golang crud restful api delete operation

Conclusion

With this CRUD RESTful API example in Golang, you’ve learned how to build and interact with a Golang API to perform the Create/Read/Update/Delete operations against a Postgres database. You also learned how to create database models and run the migrations with the GORM library.

You can find the source code of the Golang CRUD API from this GitHub Repository.