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?
- CLI Application - Build command-line applications
- Inversion of Control - Deep dive into dependency injection
- Auto Configuration - Create custom starters