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:
- How to Setup Golang GORM RESTful API Project with Postgres
- API with Golang + GORM + PostgreSQL: Access & Refresh Tokens
- Golang and GORM – User Registration and Email Verification
- Forgot/Reset Passwords in Golang with HTML Email
- 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.
After logging in, expand the dropdowns and open the query tool on the posts table to see the columns added by GORM.
Creating CRUD Functions in a RESTful API
The aim of this tutorial is to implement the following CRUD operations on the Golang API:
RESOURCE | HTTP METHOD | ROUTE | DESCRIPTION |
---|---|---|---|
posts | GET | /api/posts | Fetch all posts |
posts | POST | /api/posts | Add a new post |
posts | GET | /api/posts/:postId | Retrieve a single post |
posts | PUT | /api/posts/:postId | Update a post |
posts | DELETE | /api/posts/:postId | Delete 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.
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.
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.
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.
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.
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.
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.
Why do you need create two other structs “CreatePostRequest”, “UpdatePost” and call ShouldBindJSON on them? Why not directly parse the incoming JSON data into the original Post model like this: ctx.ShouldBindJSON(&models.Post)?
You can use the
models.Post{}
struct for both the Create and Update operations but it’s not recommended.The
models.Post{}
struct is the database model that GORM will use to generate the underlying SQL table.Also, the
models.Post{}
struct doesn’t have validation tags that Gin Gonic will use to validate the request body.The
CreatePostRequest{}
struct has fields and validation tags that are specific to the Create operation.We created the
UpdatePost{}
struct to list the fields specific to the update operation and to make all the fields optional since the HTTP method is PATCH. That means you can update any field in the database without the need to provide all the fields of the table.You can take the
models.Post{}
struct as the database model. TheCreatePostRequest{}
andUpdatePost{}
structs can be considered as the validation schemas.Hello there Edem,
i recently deployed my own version of the project to Render. Everything worked fine on my PC. I use Thunder Client extension on VS Code to test the endpoints because I get a lot of 401 in Postman but work just fine in Thunder Client. Since deploying to Render the `https://jeje-tickets.onrender.com/api/users/me` returns `User not logged in`. Please what could I do to fix this ?
I just tested your server, and it appears that it doesn’t include any cookies in the response after logging in. To address this issue, I found a workaround that might be helpful:
/api/users/me
in Postman, click on the Authorization tab.Alternatively, if you prefer to use the cookie approach, double-check the way you are returning the cookies in the login route controller and make any necessary adjustments to ensure the cookies are being set correctly.
worked like a charm..thanks a lot for helping me out with this. I copied the token I got from the login route and pasted it into the token field under auth.