DEV Community

zhuyasen
zhuyasen

Posted on

Gin + Gorm Practical Guide, Implementing a Simple Q&A Community Backend Service in One Hour

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);
Enter fullscreen mode Exit fullscreen mode

Environment Setup

  1. Ensure that your environment has installed Go and a MySQL service, and import the tables mentioned above into MySQL.

  2. Install a scaffold named sponge (integrated with Gin + Gorm), which supports Windows, macOS, and Linux environments. Click to view the installation instructions for sponge.

  3. After installation, open the terminal and start the sponge UI service:

sponge run
Enter fullscreen mode Exit fullscreen mode

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:

  1. Click on the left menu bar [SQL] --> [Create Web Service].
  2. Select the database mysql, fill in the database DSN, and then click the button Get Table Names to select table names (multiple selections are allowed).
  3. 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:

Image description

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

Image description

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
}
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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) {

}
Enter fullscreen mode Exit fullscreen mode

Then add the Register and Login methods to the UsersHandler interface:

type UsersHandler interface {
    // ...
    Register(c *gin.Context)
    Login(c *gin.Context)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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,
    })
}
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

(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 ./
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)