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 named go-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/v2
go get github.com/google/uuid
go get github.com/go-playground/validator/v10
go get -u gorm.io/gorm
go get gorm.io/driver/postgres
go 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 a main.go
file and add the following code:
main.go
package main
import (
"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 port 8000. To automatically restart the server whenever you make changes, install the Air binary with the following:
go install github.com/air-verse/air@latest
After installing Air, stop the currently running server and run:
air
This will start the server with automatic reloading enabled. Now, open your browser and go to http://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 named docker-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 an app.env
file. To ensure these credentials are available to Docker Compose, create an app.env
file in your root directory and include the following environment variables:
app.env
POSTGRES_HOST=127.0.0.1
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password123
POSTGRES_DB=golang-gorm
POSTGRES_PORT=6500
PORT=8000
PGADMIN_DEFAULT_EMAIL=admin@admin.com
PGADMIN_DEFAULT_PASSWORD=password123
With the environment variables set, run the command docker-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 the app.env
file so that we can access them within our application.
To do this, create a new directory called initializers
at the root level of your project. Within the initializers
directory, create a file named env.go
and add the following code:
initializers/env.go
package initializers
import (
"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 the AutoMigrate
function provided by GORM to apply our migrations to the Postgres schema whenever we start our Fiber server.
Navigate to the initializers
directory and create a file named db.go
, then insert the following code:
initializers/db.go
package initializers
import (
"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.DB
func ConnectDB(env *Env) {
var err error
dsn := 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 the logger.Default.LogMode()
method.
Create the Database Model and Request Schemas
Since we referenced the models.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 called models
in the root directory of your project.
Inside the models
directory, create a file named feedback.model.go
and include the following code:
models/feedback.model.go
package models
import (
"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 []*ErrorResponse
err := validate.Struct(payload)
if err != nil {
for _, err := range err.(validator.ValidationErrors) {
var element ErrorResponse
element.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 the ValidateStruct
function to validate the data according to the defined validation rules.
Once the data passes validation, the DB.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 a controllers
directory. Within this directory, create a feedback.controller.go
file and include the following code:
controllers/feedback.controller.go
func CreateFeedbackHandler(c *fiber.Ctx) error {
var payload *models.CreateFeedbackSchema
if 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 the feedback.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) * intLimit
var feedbacks []models.Feedback
results := 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 the page
and limit
query parameters from the request URL. We then parsed these parameters into integers and calculated the offset accordingly.
Using the limit
, 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 the feedback.controller.go
file.
controllers/feedback.controller.go
func FindFeedbackByIdHandler(c *fiber.Ctx) error {
feedbackId := c.Params("feedbackId")
var feedback models.Feedback
result := 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 the feedback.controller.go
file.
controllers/feedback.controller.go
func UpdateFeedbackHandler(c *fiber.Ctx) error {
feedbackId := c.Params("feedbackId")
var payload *models.UpdateFeedbackSchema
if err := c.BodyParser(&payload); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
}
var feedback models.Feedback
result := 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 your feedback.controller.go
file.
controllers/feedback.controller.go
package controllers
import (
"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.CreateFeedbackSchema
if 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) * intLimit
var feedbacks []models.Feedback
results := 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.UpdateFeedbackSchema
if err := c.BodyParser(&payload); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
}
var feedback models.Feedback
result := 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.Feedback
result := 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 the main.go
file and replace its existing content with the code provided below.
main.go
package main
import (
"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 running air
, provided you have the Air binary installed, or by using the command go 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 as http://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!