Gin + Gorm Practical Guide, Implementing a Simple Q&A Community Backend Service in One Hour
Tags: go, gin, gorm, sponge
Published: 2024-04-24 10:01:07


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

<br>

### Database Design

There are three tables in total: users, questions, and answers, as shown below:

```sql
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);
```

<br>

### 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](https://github.com/zhufuyi/sponge/blob/main/assets/install-en.md).

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

```bash  
sponge run
```

Visit `http://localhost:24631` in your browser to access the UI interface generated by sponge.

<br>

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4zrwno6bmocy0gp8cy1g.png)

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:

```bash
# Generate Swagger documentation
make docs

# Compile and run the service
make run
```

Open [http://localhost:8080/swagger/index.html](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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xwdchfj2p0ihbe46c80o.png)

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.

<br>

### 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:

```go
// 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
}
```

<br>

**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:

```go
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
)
```

<br>

**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:

```go
// 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:

```go
type UsersHandler interface {
	// ...
	Register(c *gin.Context)
	Login(c *gin.Context)
}
```

<br>

**4. Register Routes**

Navigate to the directory `internal/routers`, open the file `users.go`, register the routes for Register and Login:

```go
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:

```go
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)
	}
}
```

<br>

**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:

```go
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,
	})
}
```

<br>

**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:

```go
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.

```go
// @Security BearerAuth
```

<br>

**7. Test APIs**

After writing the business logic code, execute the following commands in the terminal:

```bash
# Generate Swagger documentation
make docs

# Compile and run the service
make run
```

Refresh [http://localhost:8080/swagger/index.html](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.

<br>

**8. Deployment**

Deployment supports three methods: server, Docker, and Kubernetes.

**(1) Deploying the Service to a Remote Linux Server**

```bash
# 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**

```bash
# 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**

```bash
# 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 ./
```

<br>

### 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](https://github.com/zhufuyi/sponge).

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.