CRUD Operations on PostgreSQL with a Golang REST API
In this article, you will learn how to create a CRUD REST API in Golang using Fiber as the web framework and PostgreSQL as the database. By the end of this tutorial, you will be able to create, read, update, and delete feedback entries in the PostgreSQL database using HTTP requests.
We will start from scratch, covering everything from setting up the Go project and spinning up a PostgreSQL server using Docker, to connecting the Go application to the PostgreSQL database and creating route controllers and corresponding routes.
More practice:
- Perform CRUD Operations with the net/http Go Standard Library
- Golang CRUD API Example with GORM and MySQL
- Golang CRUD RESTful API with SQLC and PostgreSQL
- Create CRUD API in Golang using Fiber and GORM
- Build a Simple API with Rust and Rocket
- Build a CRUD API with Rust and MongoDB
- Build a Simple API with Rust and Actix Web
- Build a CRUD API with Node.js and Sequelize

Run and Test the REST API
To get the Go API project up and running on your machine, follow these simple steps:
- Download or clone the Golang REST API project from its GitHub repository:https://github.com/wpcodevo/go-postgres-crud-rest-api. Afterwards, open the project in your preferred IDE or text editor.
- Make sure Docker is running on your computer, then open the terminal in the project’s root directory and run
docker-compose up -d
to start the PostgreSQL server. - Next, run the command
go run main.go
. This will install all necessary packages, migrate the GORM schema to the PostgreSQL database, and start the Fiber HTTP server. - First, import the
Feedback App.postman_collection.json
file from the root directory into Postman or the Thunder Client extension in VS Code. This collection includes predefined CRUD HTTP requests, sample request data, and other settings to streamline testing the CRUD endpoints. - With the collection file now imported into Postman, you can test the various CRUD endpoints by sending requests to the Go server.
Set Up the Golang Project
To start, we need to initialize the Go project. Navigate to your desktop or any preferred location where you’d like to store the source code, and create a new directory namedgo-postgres-crud-rest-api
. You can name the project folder anything you like.
Once the directory is created, open it in your IDE or text editor. In the integrated terminal, run the following command to initialize the folder as a Go project. Be sure to replace<github_username>
with your GitHub username:
go mod init github.com//go-postgres-crud-rest-api
Next, install the dependencies required for building the REST API. This tutorial will use the Fiber framework to handle HTTP requests and GORM to interact with a PostgreSQL database. Run the following commands to add these dependencies:
go get github.com/gofiber/fiber/v2go get github.com/google/uuidgo get github.com/go-playground/validator/v10go get -u gorm.io/gormgo get gorm.io/driver/postgresgo get github.com/spf13/viper
fiber
– A web framework inspired by Express.js. It provides routing, middleware, and other tools for creating REST APIs efficiently in Go.uuid
– A package for generating universally unique identifiers (UUIDs) in Go.validator
– A package that provides a set of tools for validating struct fields.gorm
– An ORM (Object-Relational Mapping) library for Go.postgres
– A PostgreSQL driver for GORM.viper
– A configuration management package that loads configuration values from different sources, like environment variables or config files.
With the dependencies installed, let’s set up a basic Fiber HTTP server to get us started before diving into the actual CRUD implementation. We’ll create a health check endpoint that returns a simple message in a JSON object.
In the root directory of your project, create amain.go
file and add the following code:
main.go
package mainimport ("log""github.com/gofiber/fiber/v2")func main() {app := fiber.New()app.Get("/api/healthchecker", func(c *fiber.Ctx) error {return c.Status(200).JSON(fiber.Map{"status": "success","message": "CRUD Operations on PostgreSQL using Golang REST API",})})log.Fatal(app.Listen(":8000"))}
Next, start the Fiber HTTP server by running:
go run main.go
Within a few seconds, the server should be listening on port8000. To automatically restart the server whenever you make changes, install theAir binary with the following:
go install github.com/air-verse/air@latest
After installingAir, stop the currently running server and run:
air
This will start the server with automatic reloading enabled. Now, open your browser and go tohttp://localhost:8000/api/healthchecker
. You should see a JSON response indicating that the Go server is running correctly.

Set Up PostgreSQL with Docker
Next, let’s set up a PostgreSQL database using Docker. You can skip this step if you plan to use a cloud-based PostgreSQL database instead.
In the root directory of your project, create a file nameddocker-compose.yml
and add the following configuration:
docker-compose.yml
services: postgres: image: postgres:latest container_name: postgres ports: - '6500:5432' volumes: - progresDB:/var/lib/postgresql/data env_file: - ./app.env pgAdmin: image: dpage/pgadmin4 container_name: pgAdmin env_file: - ./app.env ports: - '5050:80'volumes: progresDB:
In addition to the Postgres service, we have included a pgAdmin server, which provides a web-based tool for managing and administering PostgreSQL databases.
In the Docker Compose configuration, both the Postgres and pgAdmin services retrieve their secrets from anapp.env
file. To ensure these credentials are available to Docker Compose, create anapp.env
file in your root directory and include the following environment variables:
app.env
POSTGRES_HOST=127.0.0.1POSTGRES_USER=postgresPOSTGRES_PASSWORD=password123POSTGRES_DB=golang-gormPOSTGRES_PORT=6500PORT=8000PGADMIN_DEFAULT_EMAIL=admin@admin.comPGADMIN_DEFAULT_PASSWORD=password123
With the environment variables set, run the commanddocker-compose up -d
to start the Postgres and pgAdmin servers in their respective Docker containers.

Load the Environment Variables
Next, let’s use the Viper package to load the environment variables from theapp.env
file so that we can access them within our application.
To do this, create a new directory calledinitializers
at the root level of your project. Within theinitializers
directory, create a file namedenv.go
and add the following code:
initializers/env.go
package initializersimport ("github.com/spf13/viper")type Env struct {DBHost string `mapstructure:"POSTGRES_HOST"`DBUserName string `mapstructure:"POSTGRES_USER"`DBUserPassword string `mapstructure:"POSTGRES_PASSWORD"`DBName string `mapstructure:"POSTGRES_DB"`DBPort string `mapstructure:"POSTGRES_PORT"`ServerPort string `mapstructure:"PORT"`}func LoadEnv(path string) (Env Env, err error) {viper.AddConfigPath(path)viper.SetConfigType("env")viper.SetConfigName("app")viper.AutomaticEnv()err = viper.ReadInConfig()if err != nil {return}err = viper.Unmarshal(&Env)return}
Connect the App to the PostgreSQL Database
Now, let’s create the code that will enable our application to connect to the PostgreSQL server. We will also use theAutoMigrate
function provided by GORM to apply our migrations to the Postgres schema whenever we start our Fiber server.
Navigate to theinitializers
directory and create a file nameddb.go
, then insert the following code:
initializers/db.go
package initializersimport ("fmt""log""github.com/wpcodevo/go-postgres-crud-rest-api/models""gorm.io/driver/postgres""gorm.io/gorm""gorm.io/gorm/logger")var DB *gorm.DBfunc ConnectDB(env *Env) {var err errordsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Shanghai", env.DBHost, env.DBUserName, env.DBUserPassword, env.DBName, env.DBPort)DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})if err != nil {log.Fatal("Failed to connect to the Database")}DB.Logger = logger.Default.LogMode(logger.Info)DB.AutoMigrate(&models.Feedback{})fmt.Println("🚀 Connected Successfully to the Database")}
In the code above, we configured GORM to output logs related to database operations in the terminal by using thelogger.Default.LogMode()
method.
Create the Database Model and Request Schemas
Since we referenced themodels.Feedback{}
struct in the database connection code but haven’t created it yet, let’s proceed to define it.
To create our database model along with the validation logic, start by creating a new folder calledmodels
in the root directory of your project.
Inside themodels
directory, create a file namedfeedback.model.go
and include the following code:
models/feedback.model.go
package modelsimport ("time""github.com/go-playground/validator/v10""github.com/google/uuid""gorm.io/gorm")type Feedback struct {ID string `gorm:"type:char(36);primary_key" json:"id,omitempty"`Name string `gorm:"not null" json:"name,omitempty"`Email string `gorm:"not null" json:"email,omitempty"`Feedback string `gorm:"uniqueIndex:idx_feedback;not null" json:"feedback,omitempty"`Rating *float32 `gorm:"not null" json:"rating,omitempty"`Status string `json:"status,omitempty"`CreatedAt time.Time `gorm:"not null;default:'1970-01-01 00:00:01'" json:"createdAt,omitempty"`UpdatedAt time.Time `gorm:"not null;default:'1970-01-01 00:00:01';ON UPDATE CURRENT_TIMESTAMP" json:"updatedAt,omitempty"`}func (feedback *Feedback) BeforeCreate(tx *gorm.DB) (err error) {feedback.ID = uuid.New().String()return nil}var validate = validator.New()type ErrorResponse struct {Field string `json:"field"`Tag string `json:"tag"`Value string `json:"value,omitempty"`}func ValidateStruct[T any](payload T) []*ErrorResponse {var errors []*ErrorResponseerr := validate.Struct(payload)if err != nil {for _, err := range err.(validator.ValidationErrors) {var element ErrorResponseelement.Field = err.StructNamespace()element.Tag = err.Tag()element.Value = err.Param()errors = append(errors, &element)}}return errors}type CreateFeedbackSchema struct {Name string `json:"name" validate:"required"`Email string `json:"email" validate:"required"`Feedback string `json:"feedback" validate:"required"`Rating *float32 `json:"rating" validate:"required"`Status string `json:"status,omitempty"`}type UpdateFeedbackSchema struct {Name string `json:"name,omitempty"`Email string `json:"email,omitempty"`Feedback string `json:"feedback,omitempty"`Rating *float32 `json:"rating,omitempty"`Status string `json:"status,omitempty"`}
ValidateStruct
– This function allows us to validate the data fields included in the incoming request body.CreateFeedbackSchema
– This struct defines the expected format of the incoming data when creating new feedback.UpdateFeedbackSchema
– This struct defines the expected format of the incoming data when updating an existing feedback entry in the application.
Perform the CRUD Operations
Now that we have established a connection between our application and the running PostgreSQL database, we can proceed to implement the CRUD endpoint handlers that will perform the necessary operations on the database.
Handle the Create Operation
Let’s begin with the Create operation. We will start by extracting the data from the incoming request body. Next, we will use theValidateStruct
function to validate the data according to the defined validation rules.
Once the data passes validation, theDB.Create
function will insert the new feedback into the database. If the operation is successful, a copy of the feedback will be returned to the user in a JSON response.
To implement this, create acontrollers
directory. Within this directory, create afeedback.controller.go
file and include the following code:
controllers/feedback.controller.go
func CreateFeedbackHandler(c *fiber.Ctx) error {var payload *models.CreateFeedbackSchemaif err := c.BodyParser(&payload); err != nil {return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})}errors := models.ValidateStruct(payload)if errors != nil {return c.Status(fiber.StatusBadRequest).JSON(errors)}now := time.Now()newFeedback := models.Feedback{Name: payload.Name,Email: payload.Email,Feedback: payload.Feedback,Rating: payload.Rating,Status: payload.Status,CreatedAt: now,UpdatedAt: now,}result := initializers.DB.Create(&newFeedback)if result.Error != nil && strings.Contains(result.Error.Error(), "duplicate key value violates unique") {return c.Status(fiber.StatusConflict).JSON(fiber.Map{"status": "fail", "message": "Feedback already exists"})} else if result.Error != nil {return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": result.Error.Error()})}return c.Status(fiber.StatusCreated).JSON(fiber.Map{"status": "success", "data": fiber.Map{"note": newFeedback}})}
Handle the Read Operation
Next, let’s implement the Read operations. We will create two route handlers: one will allow the user to retrieve a paginated list of feedback items from the database, while the other will enable the user to retrieve a single feedback item by its ID.
Let’s begin with the handler for retrieving multiple feedback items. In thefeedback.controller.go
file, add the following code:
controllers/feedback.controller.go
func FindFeedbacksHandler(c *fiber.Ctx) error {var page = c.Query("page", "1")var limit = c.Query("limit", "10")intPage, _ := strconv.Atoi(page)intLimit, _ := strconv.Atoi(limit)offset := (intPage - 1) * intLimitvar feedbacks []models.Feedbackresults := initializers.DB.Limit(intLimit).Offset(offset).Find(&feedbacks)if results.Error != nil {return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": results.Error})}return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "results": len(feedbacks), "feedbacks": feedbacks})}
In the code above, we extracted thepage
andlimit
query parameters from the request URL. We then parsed these parameters into integers and calculated the offset accordingly.
Using thelimit
,offset
, and GORM, we queried the database to retrieve a paginated list of feedback items. Finally, we returned the feedback items retrieved by GORM to the user in a JSON object.
Now, let’s move on to retrieving a single feedback item by its ID. To implement this functionality, add the following code to thefeedback.controller.go
file.
controllers/feedback.controller.go
func FindFeedbackByIdHandler(c *fiber.Ctx) error {feedbackId := c.Params("feedbackId")var feedback models.Feedbackresult := initializers.DB.First(&feedback, "id = ?", feedbackId)if err := result.Error; err != nil {if err == gorm.ErrRecordNotFound {return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"status": "fail", "message": "No feedback with that Id exists"})}return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "fail", "message": err.Error()})}return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "data": fiber.Map{"feedback": feedback}})}
In the code above, we extracted the feedback ID from the request URL parameter. Using GORM, we attempted to retrieve the corresponding feedback item from the database. If the item is not found, we respond with a 404 error. Conversely, if a feedback item is found, we return it in the JSON response.
Handle the Update Operation
Now, let’s implement the Update operation. This process is similar to retrieving a single feedback item; however, this time we will update the fields of the feedback item based on the data provided in the request body. To achieve this, add the following code to thefeedback.controller.go
file.
controllers/feedback.controller.go
func UpdateFeedbackHandler(c *fiber.Ctx) error {feedbackId := c.Params("feedbackId")var payload *models.UpdateFeedbackSchemaif err := c.BodyParser(&payload); err != nil {return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})}var feedback models.Feedbackresult := initializers.DB.First(&feedback, "id = ?", feedbackId)if err := result.Error; err != nil {if err == gorm.ErrRecordNotFound {return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"status": "fail", "message": "No feedback with that Id exists"})}return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "fail", "message": err.Error()})}updates := make(map[string]interface{})if payload.Name != "" {updates["name"] = payload.Name}if payload.Email != "" {updates["email"] = payload.Email}if payload.Feedback != "" {updates["feedback"] = payload.Feedback}if payload.Status != "" {updates["status"] = payload.Status}if payload.Rating != nil {updates["rating"] = payload.Rating}updates["updated_at"] = time.Now()initializers.DB.Model(&feedback).Updates(updates)return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "data": fiber.Map{"feedback": feedback}})}
Let’s break down the steps taken in the code above. First, we extracted the feedback item to be updated from the query parameters. Next, we queried the database to retrieve the record that matches the provided ID.
If a record is found, we update the fields with the data from the request body. Finally, we save the changes to the database and return a copy of the updated feedback item in the JSON response.
Handle the Delete Operation
Let’s now handle the final operation: Delete. This implementation is straightforward; we will query the database to find the feedback item that matches the provided ID and then remove it. Below is the implementation:
controllers/feedback.controller.go
func DeleteFeedbackHandler(c *fiber.Ctx) error {feedbackId := c.Params("feedbackId")result := initializers.DB.Delete(&models.Feedback{}, "id = ?", feedbackId)if result.RowsAffected == 0 {return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"status": "fail", "message": "No note with that Id exists"})} else if result.Error != nil {return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": result.Error})}return c.SendStatus(fiber.StatusNoContent)}
The Complete CRUD Code
If you skipped the implementation of the CRUD handlers, you can catch up by adding the complete CRUD code provided below to yourfeedback.controller.go
file.
controllers/feedback.controller.go
package controllersimport ("strconv""strings""time""github.com/gofiber/fiber/v2""github.com/wpcodevo/go-postgres-crud-rest-api/initializers""github.com/wpcodevo/go-postgres-crud-rest-api/models""gorm.io/gorm")func CreateFeedbackHandler(c *fiber.Ctx) error {var payload *models.CreateFeedbackSchemaif err := c.BodyParser(&payload); err != nil {return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})}errors := models.ValidateStruct(payload)if errors != nil {return c.Status(fiber.StatusBadRequest).JSON(errors)}now := time.Now()newFeedback := models.Feedback{Name: payload.Name,Email: payload.Email,Feedback: payload.Feedback,Rating: payload.Rating,Status: payload.Status,CreatedAt: now,UpdatedAt: now,}result := initializers.DB.Create(&newFeedback)if result.Error != nil && strings.Contains(result.Error.Error(), "duplicate key value violates unique") {return c.Status(fiber.StatusConflict).JSON(fiber.Map{"status": "fail", "message": "Feedback already exists"})} else if result.Error != nil {return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": result.Error.Error()})}return c.Status(fiber.StatusCreated).JSON(fiber.Map{"status": "success", "data": fiber.Map{"note": newFeedback}})}func FindFeedbacksHandler(c *fiber.Ctx) error {var page = c.Query("page", "1")var limit = c.Query("limit", "10")intPage, _ := strconv.Atoi(page)intLimit, _ := strconv.Atoi(limit)offset := (intPage - 1) * intLimitvar feedbacks []models.Feedbackresults := initializers.DB.Limit(intLimit).Offset(offset).Find(&feedbacks)if results.Error != nil {return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": results.Error})}return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "results": len(feedbacks), "feedbacks": feedbacks})}func UpdateFeedbackHandler(c *fiber.Ctx) error {feedbackId := c.Params("feedbackId")var payload *models.UpdateFeedbackSchemaif err := c.BodyParser(&payload); err != nil {return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})}var feedback models.Feedbackresult := initializers.DB.First(&feedback, "id = ?", feedbackId)if err := result.Error; err != nil {if err == gorm.ErrRecordNotFound {return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"status": "fail", "message": "No feedback with that Id exists"})}return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "fail", "message": err.Error()})}updates := make(map[string]interface{})if payload.Name != "" {updates["name"] = payload.Name}if payload.Email != "" {updates["email"] = payload.Email}if payload.Feedback != "" {updates["feedback"] = payload.Feedback}if payload.Status != "" {updates["status"] = payload.Status}if payload.Rating != nil {updates["rating"] = payload.Rating}updates["updated_at"] = time.Now()initializers.DB.Model(&feedback).Updates(updates)return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "data": fiber.Map{"feedback": feedback}})}func FindFeedbackByIdHandler(c *fiber.Ctx) error {feedbackId := c.Params("feedbackId")var feedback models.Feedbackresult := initializers.DB.First(&feedback, "id = ?", feedbackId)if err := result.Error; err != nil {if err == gorm.ErrRecordNotFound {return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"status": "fail", "message": "No feedback with that Id exists"})}return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "fail", "message": err.Error()})}return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "data": fiber.Map{"feedback": feedback}})}func DeleteFeedbackHandler(c *fiber.Ctx) error {feedbackId := c.Params("feedbackId")result := initializers.DB.Delete(&models.Feedback{}, "id = ?", feedbackId)if result.RowsAffected == 0 {return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"status": "fail", "message": "No note with that Id exists"})} else if result.Error != nil {return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": result.Error})}return c.SendStatus(fiber.StatusNoContent)}
Set Up CORS and the CRUD Endpoints
It’s time to create routes with Fiber that will invoke our CRUD handlers. We will also set up CORS on the server to allow it to accept requests from cross-origin domains.
To do this, open themain.go
file and replace its existing content with the code provided below.
main.go
package mainimport ("log""github.com/gofiber/fiber/v2""github.com/gofiber/fiber/v2/middleware/cors""github.com/gofiber/fiber/v2/middleware/logger""github.com/wpcodevo/go-postgres-crud-rest-api/controllers""github.com/wpcodevo/go-postgres-crud-rest-api/initializers")func main() {env, err := initializers.LoadEnv(".")if err != nil {log.Fatal("🚀 Could not load environment variables", err)}initializers.ConnectDB(&env)app := fiber.New()micro := fiber.New()app.Mount("/api", micro)app.Use(logger.New())app.Use(cors.New(cors.Config{AllowOrigins: "http://localhost:3000",AllowHeaders: "Origin, Content-Type, Accept",AllowMethods: "GET, POST, PATCH, DELETE",AllowCredentials: true,}))micro.Route("/feedbacks", func(router fiber.Router) {router.Post("/", controllers.CreateFeedbackHandler)router.Get("", controllers.FindFeedbacksHandler)})micro.Route("/feedbacks/:feedbackId", func(router fiber.Router) {router.Get("", controllers.FindFeedbackByIdHandler)router.Patch("", controllers.UpdateFeedbackHandler)router.Delete("", controllers.DeleteFeedbackHandler)})micro.Get("/healthchecker", func(c *fiber.Ctx) error {return c.Status(200).JSON(fiber.Map{"status": "success","message": "CRUD Operations on PostgreSQL using Golang REST API",})})log.Fatal(app.Listen(":" + env.ServerPort))}
We have completed the REST API implementation. You can start the Fiber HTTP server by runningair
, provided you have the Air binary installed, or by using the commandgo run main.go
.
Once the server is up and running, you can use the Postman extension in VS Code to make requests and test the CRUD functionality.
Let me demonstrate how to create a feedback item. Define the URL ashttp://localhost:8000/api/feedbacks/
and set the HTTP method to POST.
Next, click on the “Body” tab and add the following data, ensuring that the body type is set to JSON.
{ "name": "John Doe", "email": "johndoe@gmail.com", "feedback": "Thanks CodevoWeb. I improved my Rust skills by following your Rust articles.", "rating": 4.5, "status": "active"}
Once you have completed this, send the request. The server will insert the feedback into the database and respond with a copy of the newly created feedback in the JSON response.

Conclusion
In this comprehensive tutorial, you learned how to perform CRUD operations on a PostgreSQL database using a Golang REST API. I hope you found this article both informative and enjoyable. If you have any questions or feedback, please feel free to leave them in the comment section. Thank you for reading!
Tags:
GolangHow to Validate React.js Forms Without External Libraries
JSON Web Token Authentication and Authorization in Golang
Leave a ReplyCancel reply
This site uses Akismet to reduce spam.Learn how your comment data is processed.
Support Me!

Recent posts
Categories
- C#(2)
- C++(1)
- CSS / SCSS(3)
- Deno(8)
- Golang(31)
- JavaScript(5)
- NextJs(38)
- NodeJS(32)
- Programming(19)
- Python(19)
- React(38)
- Rust(35)
- Svelte(5)
- Vue(7)