Web Application

Features

  • Web MVC (Model-View-Controller)
  • Auto Configuration with properties configs for dependency injection
  • Dependency injection with the struct tag inject:"" or constructor injection
  • Automatic route mapping based on method names
  • Built-in validation support
  • Swagger/OpenAPI documentation support

Introduction to Hiboot MVC

Hiboot prefers to hide business-independent code, so that developers can concentrate on business logic.

Unlike most Go web frameworks, Hiboot does not need to manually setup routes. Hiboot uses reflection to construct routes automatically based on controller method names.

Project Structure

.
├── config
│   ├── application.yml
│   ├── application-local.yml
│   └── application-dev.yml
├── main.go
├── main_test.go
├── controller
│   ├── user.go
│   └── user_test.go
├── entity
│   └── user.go
└── service
    ├── user.go
    └── user_test.go

Application Properties

Hiboot lets you externalize your configuration so that you can work with the same application code in different environments.

Property values can be injected directly into structs using the value:"" tag:

type MyService struct {
	AppName string `value:"${app.name}"`
	Port    int    `value:"${server.port}"`
}

config/application.yml

app:
  project: myproject
  name: myapp
  profiles:
    include:
    - actuator
    - logging

server:
  port: 8080

logging:
  level: info

Application Properties Reference

Field Description Example
app.project Project name myproject
app.name Application name myapp
app.profiles.active Active profile dev, test, prod
app.profiles.include Starters to include actuator, logging, swagger
server.port Server port 8080
logging.level Log level debug, info, warn, error

Profile-specific Properties

Profile-specific properties can be defined using the naming convention: application-${profile}.yml.

config/application-local.yml

server:
  port: 8081

logging:
  level: debug

Profile-specific files always override the base application.yml properties.

Writing the Code

main.go

The main package is the entry point for your web application:

package main

import (
	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/app/web"
	"github.com/hidevopsio/hiboot/pkg/starter/actuator"
	"github.com/hidevopsio/hiboot/pkg/starter/logging"

	_ "myapp/controller"
)

func main() {
	web.NewApplication().
		SetProperty(app.ProfilesInclude, actuator.Profile, logging.Profile).
		Run()
}

Controller - controller/user.go

Controllers handle HTTP requests. Embed at.RestController to mark a struct as a REST controller:

package controller

import (
	"myapp/entity"
	"myapp/service"
	"net/http"

	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/at"
	"github.com/hidevopsio/hiboot/pkg/model"
)

// userController handles user-related HTTP requests
type userController struct {
	at.RestController
	at.RequestMapping `value:"/user"`

	userService service.UserService
}

func init() {
	app.Register(newUserController)
}

// newUserController injects userService through constructor
func newUserController(userService service.UserService) *userController {
	return &userController{
		userService: userService,
	}
}

// Post handles POST /user
func (c *userController) Post(request *entity.User) (model.Response, error) {
	err := c.userService.AddUser(request)
	response := new(model.BaseResponse)
	response.SetData(request)
	return response, err
}

// GetById handles GET /user/id/{id}
func (c *userController) GetById(id uint64) (response model.Response, err error) {
	user, err := c.userService.GetUser(id)
	response = new(model.BaseResponse)
	if err != nil {
		response.SetCode(http.StatusNotFound)
	} else {
		response.SetData(user)
	}
	return
}

// GetAll handles GET /user/all
func (c *userController) GetAll() (response model.Response, err error) {
	users, err := c.userService.GetAll()
	response = new(model.BaseResponse)
	response.SetData(users)
	return
}

// DeleteById handles DELETE /user/id/{id}
func (c *userController) DeleteById(id uint64) (response model.Response, err error) {
	err = c.userService.DeleteUser(id)
	response = new(model.BaseResponse)
	return
}

HTTP Method Mapping

Method names are automatically mapped to HTTP methods:

Method Name HTTP Method Route Example
Get GET GET /user
GetById GET GET /user/id/{id}
GetAll GET GET /user/all
Post POST POST /user
Put PUT PUT /user
PutById PUT PUT /user/id/{id}
Delete DELETE DELETE /user
DeleteById DELETE DELETE /user/id/{id}

Entity - entity/user.go

Entities represent your business models:

package entity

import "github.com/hidevopsio/hiboot/pkg/model"

type User struct {
	model.RequestBody
	Id       uint64 `json:"id"`
	Name     string `json:"name" validate:"required"`
	Username string `json:"username" validate:"required"`
	Password string `json:"password" validate:"required"`
	Email    string `json:"email" validate:"required,email"`
	Age      uint   `json:"age" validate:"gte=0,lte=130"`
	Gender   uint   `json:"gender" validate:"gte=0,lte=2"`
}

func (u *User) TableName() string {
	return "user"
}

Service - service/user.go

Services implement business logic:

package service

import (
	"errors"
	"myapp/entity"

	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/utils/idgen"
)

type UserService interface {
	AddUser(user *entity.User) error
	GetUser(id uint64) (*entity.User, error)
	GetAll() (*[]entity.User, error)
	DeleteUser(id uint64) error
}

type userServiceImpl struct {
	UserService
	users map[uint64]*entity.User
}

func init() {
	app.Register(newUserService)
}

func newUserService() UserService {
	return &userServiceImpl{
		users: make(map[uint64]*entity.User),
	}
}

func (s *userServiceImpl) AddUser(user *entity.User) error {
	if user == nil {
		return errors.New("user is not allowed nil")
	}
	if user.Id == 0 {
		user.Id, _ = idgen.Next()
	}
	s.users[user.Id] = user
	return nil
}

func (s *userServiceImpl) GetUser(id uint64) (*entity.User, error) {
	user, ok := s.users[id]
	if !ok {
		return nil, errors.New("user not found")
	}
	return user, nil
}

func (s *userServiceImpl) GetAll() (*[]entity.User, error) {
	users := make([]entity.User, 0, len(s.users))
	for _, u := range s.users {
		users = append(users, *u)
	}
	return &users, nil
}

func (s *userServiceImpl) DeleteUser(id uint64) error {
	delete(s.users, id)
	return nil
}

Using Annotations for Swagger

For Swagger/OpenAPI documentation, use annotations in your controller methods:

package controller

import (
	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/at"
)

type userController struct {
	at.RestController
	at.RequestMapping `value:"/user"`
}

func init() {
	app.Register(newUserController)
}

func newUserController() *userController {
	return &userController{}
}

// GetById handles GET /user/{id} with Swagger documentation
func (c *userController) GetById(
	at struct {
		at.GetMapping `value:"/{id}"`
		at.Operation  `id:"getUserById" description:"Get user by ID"`
		at.Produces   `values:"application/json"`
		Parameters    struct {
			Id struct {
				at.Parameter `name:"id" in:"path" type:"integer" description:"User ID"`
			}
		}
		Responses struct {
			StatusOK struct {
				at.Response `code:"200" description:"Success"`
			}
			NotFound struct {
				at.Response `code:"404" description:"User not found"`
			}
		}
	},
	id uint64,
) string {
	return "User details"
}

Running the Application

go run main.go

Output:

______  ____________             _____
___  / / /__(_)__  /_______________  /_
__  /_/ /__  /__  __ \  __ \  __ \  __/
_  __  / _  / _  /_/ / /_/ / /_/ / /_     Hiboot Application Framework
/_/ /_/  /_/  /_.___/\____/\____/\__/     https://hiboot.hidevops.io

[INFO] Starting Hiboot web application on localhost with PID xxx
[INFO] Mapped "/user" onto controller.userController.Post()
[INFO] Mapped "/user/id/{id}" onto controller.userController.GetById()
[INFO] Mapped "/user/all" onto controller.userController.GetAll()
[INFO] Mapped "/health" onto actuator.healthController.Get()
[INFO] Hiboot started on port(s) http://localhost:8080

Making Requests

Create a user

curl -X POST http://localhost:8080/user \
  -H "Content-Type: application/json" \
  -d '{"name":"John Doe","username":"johnd","password":"secret123","email":"john@example.com","age":30,"gender":1}'

Get all users

curl http://localhost:8080/user/all

Response:

{
    "code": 200,
    "data": [
        {
            "id": 209536579658580081,
            "name": "John Doe",
            "username": "johnd",
            "email": "john@example.com",
            "age": 30,
            "gender": 1
        }
    ],
    "message": "Success"
}

Unit Testing

main_test.go

package main

import (
	"testing"
	"time"
)

func TestRunMain(t *testing.T) {
	go main()
	time.Sleep(200 * time.Millisecond)
}

Controller Testing with Mocks

Use Mockery to generate mock implementations:

go install github.com/vektra/mockery/v2@latest
mockery --name UserService --dir service --output service/mocks

Then write your test:

package controller

import (
	"myapp/entity"
	"myapp/service/mocks"
	"net/http"
	"testing"

	"github.com/hidevopsio/hiboot/pkg/app/web"
	"github.com/hidevopsio/hiboot/pkg/utils/idgen"
	"github.com/stretchr/testify/assert"
)

func TestUserController(t *testing.T) {
	mockUserService := new(mocks.UserService)
	userController := newUserController(mockUserService)
	testApp := web.RunTestApplication(t, userController)

	id, err := idgen.Next()
	assert.NoError(t, err)

	testUser := &entity.User{
		Id:       id,
		Name:     "Test User",
		Username: "testuser",
		Password: "password123",
		Email:    "test@example.com",
		Age:      25,
		Gender:   1,
	}

	mockUserService.On("AddUser", testUser).Return(nil)
	t.Run("should add user with POST request", func(t *testing.T) {
		testApp.Post("/user").
			WithJSON(testUser).
			Expect().Status(http.StatusOK)
	})

	mockUserService.On("GetUser", id).Return(testUser, nil)
	t.Run("should get user with GET request", func(t *testing.T) {
		testApp.Get("/user/id/{id}").
			WithPath("id", id).
			Expect().Status(http.StatusOK)
	})

	mockUserService.AssertExpectations(t)
}

What’s Next?