Gin + Gorm Practical Guide, Implementing a Simple Q&A Community Backend Service in One Hour
zhuyasen
Posted on April 24, 2024
Q&A communities are a common type of social application that allows users to post questions, answer questions, and interact with each other. With the development of the Internet, Q&A communities have become important platforms for people to acquire knowledge and share experiences.
This article will introduce how to build a simple Q&A community using Gin and Gorm. The community includes the following features:
- User registration and login
- Question posting and answering
- Question list and details
- Answer list and details
- User information and answer lists
Database Design
There are three tables in total: users, questions, and answers, as shown below:
CREATE DATABASE IF NOT EXISTS qasys DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
create table qasys.users
(
id bigint unsigned auto_increment primary key,
username varchar(255) not null,
password varchar(255) not null,
email varchar(255) not null,
created_at datetime null,
updated_at datetime null,
deleted_at datetime null,
constraint email unique (email),
constraint username unique (username)
);
create table qasys.questions
(
id bigint unsigned auto_increment primary key,
user_id int not null,
title varchar(255) not null,
content text not null,
created_at datetime null,
updated_at datetime null,
deleted_at datetime null
);
create index questions_user_id_index on qasys.questions (user_id);
create table qasys.answers
(
id bigint unsigned auto_increment primary key,
question_id int not null,
user_id int not null,
content text not null,
created_at datetime null,
updated_at datetime null,
deleted_at datetime null
);
create index answers_question_id_index on qasys.answers (question_id);
create index answers_user_id_index on qasys.answers (user_id);
Environment Setup
Ensure that your environment has installed Go and a MySQL service, and import the tables mentioned above into MySQL.
Install a scaffold named sponge (integrated with Gin + Gorm), which supports Windows, macOS, and Linux environments. Click to view the installation instructions for sponge.
After installation, open the terminal and start the sponge UI service:
sponge run
Visit http://localhost:24631
in your browser to access the UI interface generated by sponge.
Creating the Q&A Community Service
Enter the sponge UI interface:
- Click on the left menu bar [SQL] --> [Create Web Service].
- Select the database
mysql
, fill in thedatabase DSN
, and then click the buttonGet Table Names
to select table names (multiple selections are allowed). - Fill in other parameters. Hover over the question mark
?
to view parameter descriptions.
After filling in the parameters, click the button Download Code
to generate the complete project code for the web service, as shown in the following figure:
This is the directory of the created web service code, which already includes CRUD API code for users, questions, and answers tables, along with initialization and configuration code for Gin and Gorm, ready to use out of the box.
.
├─ cmd
│ └─ qa
│ ├─ initial
│ └─ main.go
├─ configs
├─ deployments
│ ├─ binary
│ ├─ docker-compose
│ └─ kubernetes
├─ docs
├─ internal
│ ├─ cache
│ ├─ config
│ ├─ dao
│ ├─ ecode
│ ├─ handler
│ ├─ model
│ ├─ routers
│ ├─ server
│ └─ types
└─ scripts
Unzip the code files, open the terminal, navigate to the web service code directory, and execute the following command:
# Generate Swagger documentation
make docs
# Compile and run the service
make run
Open http://localhost:8080/swagger/index.html in your browser. You can perform API testing for CRUD operations on the web page, as shown in the following figure:
From the above figure, you can see that most of the APIs have been completed using sponge. The registration and login APIs, as well as authentication, are yet to be implemented. Let's proceed to complete the remaining functionalities.
Adding Registration and Login APIs
1. Define Request Parameters and Response Result Structures
Navigate to the directory internal/types
, open the file users_types.go
, and add the code for request and response structures for registration and login:
// RegisterRequest login request params
type RegisterRequest struct {
Email string `json:"email" binding:"email"`
Username string `json:"username" binding:"min=2"`
Password string `json:"password" binding:"min=6"`
}
// RegisterRespond data
type RegisterRespond struct {
Code int `json:"code"` // return code
Msg string `json:"msg"` // return information description
Data struct {
ID uint64 `json:"id"`
} `json:"data"` // return data
}
// LoginRequest login request params
type LoginRequest struct {
Username string `json:"username" binding:"min=2"`
Password string `json:"password" binding:"min=6"`
}
// LoginRespond data
type LoginRespond struct {
Code int `json:"code"` // return code
Msg string `json:"msg"` // return information description
Data struct {
ID uint64 `json:"id"`
Token string `json:"token"`
} `json:"data"` // return data
}
2. Define Error Codes
Navigate to the directory internal/ecode
, open the file users_http.go
, and add two lines of code to define error codes for registration and login:
var (
usersNO = 49
usersName = "users"
usersBaseCode = errcode.HCode(usersNO)
// ...
ErrRegisterUsers = errcode.NewError(usersBaseCode+10, "register failed")
ErrLoginUsers = errcode.NewError(usersBaseCode+11, "login failed")
// for each error code added, add +1 to the previous error code
)
3. Define Handler Functions
Navigate to the directory internal/handler
, open the file users.go
, add methods for registration and login, and fill in the Swagger annotations:
// Register register
// @Summary register
// @Description register
// @Tags auth
// @accept json
// @Produce json
// @Param data body types.RegisterRequest true "login information"
// @Success 200 {object} types.RegisterRespond{}
// @Router /api/v1/auth/register [post]
func (h *usersHandler) Register(c *gin.Context) {
}
// Login login
// @Summary login
// @Description login
// @Tags auth
// @accept json
// @Produce json
// @Param data body types.LoginRequest true "login information"
// @Success 200 {object} types.LoginRespond{}
// @Router /api/v1/teacher/login [post]
func (h *usersHandler) Login(c *gin.Context) {
}
Then add the Register and Login methods to the UsersHandler interface:
type UsersHandler interface {
// ...
Register(c *gin.Context)
Login(c *gin.Context)
}
4. Register Routes
Navigate to the directory internal/routers
, open the file users.go
, register the routes for Register and Login:
func noAuthUsersRouter(group *gin.RouterGroup) {
h := handler.NewUsersHandler()
group.POST("/auth/register", h.Register)
group.POST("/auth/login", h.Login)
}
Then add the noAuthUsersRouter
function under the registration route function in routers.go
, as shown below:
func registerRouters(r *gin.Engine, groupPath string, routerFns []func(*gin.RouterGroup), handlers ...gin.HandlerFunc) {
rg := r.Group(groupPath, handlers...)
noAuthUsersRouter(rg)
for _, fn := range routerFns {
fn(rg)
}
}
5. Write Business Logic Code
Navigate to the directory internal/handler
, open the file users.go
, and write the business logic code for registration and login:
func (h *usersHandler) Register(c *gin.Context) {
req := &types.RegisterRequest{}
err := c.ShouldBindJSON(req)
if err != nil {
logger.Warn("ShouldBindJSON error: ", logger.Err(err), middleware.GCtxRequestIDField(c))
response.Error(c, ecode.InvalidParams)
return
}
ctx := middleware.WrapCtx(c)
password, err := gocrypto.HashAndSaltPassword(req.Password)
if err != nil {
logger.Error("gocrypto.HashAndSaltPassword error", logger.Err(err), middleware.CtxRequestIDField(ctx))
response.Output(c, ecode.InternalServerError.ToHTTPCode())
return
}
users := &model.Users{
Username: req.Username,
Password: password,
Email: req.Email,
}
err = h.iDao.Create(ctx, users)
if err != nil {
logger.Error("Create error", logger.Err(err), logger.Any("form", req), middleware.GCtxRequestIDField(c))
response.Output(c, ecode.InternalServerError.ToHTTPCode())
return
}
response.Success(c, gin.H{"id": users.ID})
}
func (h *usersHandler) Login(c *gin.Context) {
req := &types.LoginRequest{}
err := c.ShouldBindJSON(req)
if err != nil {
logger.Warn("ShouldBindJSON error: ", logger.Err(err), middleware.GCtxRequestIDField(c))
response.Error(c, ecode.InvalidParams)
return
}
ctx := middleware.WrapCtx(c)
condition := &query.Conditions{
Columns: []query.Column{
{
Name: "username",
Exp: "=",
Value: req.Username,
},
},
}
user, err := h.iDao.GetByCondition(ctx, condition)
if err != nil {
if errors.Is(err, model.ErrRecordNotFound) {
logger.Warn("Login not found", logger.Err(err), logger.Any("form", req), middleware.GCtxRequestIDField(c))
response.Error(c, ecode.ErrLoginUsers)
} else {
logger.Error("Login error", logger.Err(err), logger.Any("form", req), middleware.GCtxRequestIDField(c))
response.Output(c, ecode.InternalServerError.ToHTTPCode())
}
return
}
if !gocrypto.VerifyPassword(req.Password, user.Password) {
logger.Warn("password error", middleware.CtxRequestIDField(ctx))
response.Error(c, ecode.ErrLoginUsers)
}
token, err := jwt.GenerateToken(utils.Uint64ToStr(user.ID), user.Username)
if err != nil {
logger.Error("jwt.GenerateToken error", logger.Err(err), middleware.CtxRequestIDField(ctx))
response.Output(c, ecode.InternalServerError.ToHTTPCode())
}
// TODO: save token to cache
response.Success(c, gin.H{
"id": user.ID,
"token": token,
})
}
6. Enable API Authentication
With the registration and login APIs in place, other APIs need to add JWT authentication. By default, all APIs generated by sponge do not include JWT authentication. Simply enable it. Navigate to the directory internal/routers
, open the files questions.go
, answers.go
, users.go
, and uncomment the default commented code to enable JWT authentication for all routes below:
group.Use(middleware.Auth())
Then add the following explanation to the Swagger annotation of the APIs requiring authentication. This ensures that when requesting APIs on the Swagger page, the token will be included in the request header, and the backend will obtain and verify the token's validity.
// @Security BearerAuth
7. Test APIs
After writing the business logic code, execute the following commands in the terminal:
# Generate Swagger documentation
make docs
# Compile and run the service
make run
Refresh http://localhost:8080/swagger/index.html in your browser. You can see the registration and login APIs on the page. Test the registration and login APIs on the page, obtain the token, and fill in Bearer token
in the Authorization field to test if other APIs can be called successfully.
8. Deployment
Deployment supports three methods: server, Docker, and Kubernetes.
(1) Deploying the Service to a Remote Linux Server
# If you need to update the service, execute this command again
make deploy-binary USER=root PWD=123456 IP=192.168.1.10
(2) Deployment to Docker
# Build the image
make image-build REPO_HOST=myRepo.com TAG=1.0
# Push the image. The parameters REPO_HOST and TAG here are the same as those used for building the image.
make image-push REPO_HOST=myRepo.com TAG=1.0
# Copy the files under deployments/docker-compose directory to the target server, modify the image address, and then start the service
docker-compose up -d
(3) Deployment to Kubernetes
# Build the image
make image-build REPO_HOST=myRepo.com TAG=1.0
# Push the image. The parameters REPO_HOST and TAG here are the same as those used for building the image.
make image-push REPO_HOST=myRepo.com TAG=1.0
# Copy the files under deployments/kubernetes directory to the target server, modify the image address, and then execute the scripts in order
kubectl apply -f ./*namespace.yml
kubectl apply -f ./
Conclusion
Sponge integrates powerful tools for web backend service development using Gin and Gorm. With Sponge, developers can quickly and easily build RESTful API services. Here is the Sponge GitHub repository.
This article explained how to build a simple Q&A community using Gin and Gorm. The Q&A community includes some basic functionalities and can serve as a foundation for more complex applications.
Posted on April 24, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.