
reggin
Type-safe Gin HTTP route registration with Go generics.
CHINESE README
中文说明
Reasons to Choose reggin
When building Gin applications, you often face these challenges:
- Repetitive code: Writing parameter binding, error handling, and response formatting again and again
- Type inconsistency: Response types differ between endpoints, making client code complex
- Weak organization: Routes scattered across codebase without clean structure
- Error-prone: Handwritten response building leads to inconsistent error handling
reggin solves these problems through:
- Type-safe routes: Define response types once, use everywhere with compile-time checks
- Processing wrappers: 5 different patterns (R/C/P/F/H) to match the coding scene
- Clean architecture: Organize routes into services with clean interfaces
- Automatic response handling: Framework manages JSON encoding, status codes, and error formatting
Core Concepts
1. Type-Safe Routes
Define routes with generic response types achieving compile-time validation:
type Response struct {
Code int `json:"code"`
Desc string `json:"desc"`
Data any `json:"data"`
}
type UserService struct{}
func (s *UserService) GetRoutes() reggin.Routes[Response] {
return reggin.Routes[Response]{
{Method: reggin.GET, Path: "users/:id", Handle: s.GetUserInfo},
{Method: reggin.POST, Path: "users", Handle: s.CreateUsers},
}
}
2. Handler Wrappers
5 distinct wrapping patterns to match different needs:
| Pattern |
Context |
Response Format |
Use Case |
| R (Result) |
❌ No |
Errors as messages |
Simple logic without context |
| C (Context) |
✅ Yes |
Comprehensive control |
Recommended option achieving complete features |
| P (Pure) |
❌ No |
Comprehensive control |
Pure business logic |
| F (Flexible) |
✅ Yes |
Success auto, custom errors |
Quick development |
| H (HTTP) |
✅ Yes |
Custom status code |
REST APIs needing 200/201/400/404/500 |
3. Parameter Binding
Multiple binding options supporting different request types:
// JSON binding
BIND[ArgType](ctx) // or B[ArgType](ctx)
// Query parameters
Q[ArgType](ctx)
// Custom binding
QueryJson[ArgType](ctx)
Installation
go get github.com/yylego/reggin
Quick Start
Basic Usage (Pattern F - Flexible)
// Package main demonstrates RX and FX handler patterns
// RX: Simple Result pattern without context access
// FX: Flexible pattern with context access
package main
import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/yylego/done"
"github.com/yylego/erero"
"github.com/yylego/reggin/warpginhandle"
)
func main() {
engine := gin.Default()
engine.POST("/api/tonumber", warpginhandle.RX(ToNumber, ErrorResp))
engine.POST("/api/tostring", warpginhandle.FX(ToString, ErrorResp))
done.Done(engine.Run(":8080"))
}
type ToNumberRequest struct {
Value string `json:"value"`
}
type ToNumberResponse struct {
Num int `json:"num"`
}
func ToNumber(req *ToNumberRequest) (*ToNumberResponse, error) {
num, err := strconv.Atoi(req.Value)
if err != nil {
return nil, erero.Wro(err)
}
return &ToNumberResponse{Num: num}, nil
}
type ToStringRequest struct {
Num int `json:"num"`
}
type ToStringResponse struct {
Value string `json:"value"`
}
func ToString(ctx *gin.Context, req *ToStringRequest) (*ToStringResponse, error) {
value := strconv.Itoa(req.Num)
return &ToStringResponse{Value: value}, nil
}
type ErrorResponse struct {
Code int `json:"code"`
Desc string `json:"desc"`
}
func ErrorResp(ctx *gin.Context, err error) *ErrorResponse {
return &ErrorResponse{Code: -1, Desc: err.Error()}
}
⬆️ Source: Source
Service-Based Organization (Pattern C - Context)
// Package main demonstrates PX and CX handler patterns
// PX: Pure pattern achieving testable business logic
// CX: Context pattern with complete request metadata access
package main
import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/yylego/done"
"github.com/yylego/erero"
"github.com/yylego/reggin/warpginhandle"
)
func main() {
engine := gin.Default()
engine.POST("/events/calculate", warpginhandle.PX(Calculate, MakeResp[CalculateResult]))
engine.POST("/events/transform", warpginhandle.CX(Transform, MakeResp[TransformResult]))
done.Done(engine.Run(":8080"))
}
type CalculateRequest struct {
Value string `json:"value"`
}
type CalculateResult struct {
Num int `json:"num"`
}
func Calculate(req *CalculateRequest) (*CalculateResult, error) {
num, err := strconv.Atoi(req.Value)
if err != nil {
return nil, erero.Wro(err)
}
return &CalculateResult{Num: num}, nil
}
type TransformRequest struct {
Num int `json:"num"`
}
type TransformResult struct {
Value string `json:"value"`
}
func Transform(ctx *gin.Context, req *TransformRequest) (*TransformResult, error) {
value := strconv.Itoa(req.Num)
return &TransformResult{Value: value}, nil
}
type Response struct {
Code int `json:"code"`
Desc string `json:"desc"`
Data any `json:"data"`
}
func MakeResp[RES any](ctx *gin.Context, res *RES, err error) *Response {
if err != nil {
return &Response{Code: -1, Desc: err.Error(), Data: nil}
}
return &Response{Code: 0, Desc: "SUCCESS", Data: res}
}
⬆️ Source: Source
Enterprise Architecture (Type-Safe Routes)
// Package main demonstrates regginservice package usage patterns
// Shows service registration methods achieving different routing needs
package main
import (
"github.com/gin-gonic/gin"
"github.com/yylego/done"
"github.com/yylego/reggin/regginservice"
)
func main() {
engine := gin.Default()
// Method 1: Register service at Engine level
regginservice.AddEndpoints(engine, &ServiceA{})
// Method 2: Register service to RouterGroup
regginservice.SetRouteGroup(engine, "/v1", &ServiceB{})
done.Done(engine.Run(":8080"))
}
type Response struct {
Code int `json:"code"`
Desc string `json:"desc"`
Data any `json:"data"`
}
// ServiceA implements EndpointHandler achieving Engine-level routes
type ServiceA struct{}
func (s *ServiceA) RegisterRoutes(engine *gin.Engine) {
engine.GET("/health", s.Health)
}
func (s *ServiceA) Health(c *gin.Context) {
c.JSON(200, Response{
Code: 0,
Desc: "OK",
Data: map[string]string{"status": "healthy"},
})
}
// ServiceB implements RouteGroupHandler achieving versioned routes
type ServiceB struct{}
func (s *ServiceB) RegisterRoutes(group *gin.RouterGroup) {
group.GET("/users", s.ListUsers)
group.GET("/users/:id", s.GetUser)
}
func (s *ServiceB) ListUsers(c *gin.Context) {
c.JSON(200, Response{
Code: 0,
Desc: "OK",
Data: []string{"alice", "bob", "charlie"},
})
}
func (s *ServiceB) GetUser(c *gin.Context) {
userID := c.Param("id")
c.JSON(200, Response{
Code: 0,
Desc: "OK",
Data: map[string]string{"id": userID, "name": "User " + userID},
})
}
⬆️ Source: Source
Handler Patterns Deep Dive
R Pattern (Result - Simple)
Best suited when: Simple logic without context needs
engine.POST("/api/process", warpginhandle.RX(processHandle, errorMsg))
func processHandle(arg *ProcessArg) (*ProcessRes, error) {
// No context access - pure business logic
result, err := doSomeWork(arg)
return result, err
}
func errorMsg(ctx *gin.Context, err error) *ErrorResponse {
return &ErrorResponse{Code: -1, Message: err.Error()}
}
Features:
- ❌ No context access
- ✅ Simplest signature
- ✅ Failures use custom messages
- ✅ Success cases give results as-is
C Pattern (Context - Recommended)
Best suited when: Complete feature development needing comprehensive control
engine.POST("/api/process", warpginhandle.CX(processHandle, makeResp[ProcessRes]))
func processHandle(ctx *gin.Context, arg *ProcessArg) (*ProcessRes, error) {
// Complete context access to headers, auth, etc
userID := ctx.GetHeader("User-ID")
result, err := doWork(arg, userID)
return result, err
}
func makeResp[RES any](ctx *gin.Context, res *RES, err error) *Response {
if err != nil {
return &Response{Code: -1, Desc: err.Error(), Data: nil}
}
return &Response{Code: 0, Desc: "SUCCESS", Data: res}
}
Features:
- ✅ Context access
- ✅ Comprehensive response control
- ✅ Unified response format
- ✅ Best choice when maintaining team standards
P Pattern (Pure - Business Logic)
Best suited when: Testable pure functions needing response wrapping
engine.POST("/api/process", warpginhandle.PX(processHandle, makeResp[ProcessRes]))
func processHandle(arg *ProcessArg) (*ProcessRes, error) {
// Pure business logic - simple to test
return calculate(arg)
}
func makeResp[RES any](ctx *gin.Context, res *RES, err error) *Response {
// Wrap result in standard response
if err != nil {
return &Response{Code: -1, Desc: err.Error()}
}
return &Response{Code: 0, Data: res}
}
Features:
- ❌ No context in business logic
- ✅ Simple unit testing
- ✅ Comprehensive response control
- ✅ Clean separation of concerns
F Pattern (Flexible - Fast Development)
Best suited when: Quick development needing direct result control
engine.POST("/api/process", warpginhandle.FX(processHandle, errorMsg))
func processHandle(ctx *gin.Context, arg *ProcessArg) (*ProcessRes, error) {
// Context available, direct result control
return &ProcessRes{Value: arg.Input * 2}, nil
}
func errorMsg(ctx *gin.Context, err error) *ErrorResponse {
return &ErrorResponse{Code: -1, Message: err.Error()}
}
Features:
- ✅ Context access
- ✅ Success gives results as-is
- ✅ Failures use custom format
- ✅ Fast to write
H Pattern (HTTP - Status-Aware)
Best suited when: REST APIs where different errors need distinct HTTP status codes
engine.POST("/api/items", warpginhandle.HX(createItem, errorMsg))
engine.POST("/api/items/get", warpginhandle.HX(getItem, errorMsg))
func getItem(ctx *gin.Context, arg *GetItemReq) (*Item, int, error) {
if arg.ID <= 0 {
return nil, 400, errors.New("bad id")
}
item, err := findItem(arg.ID)
if err != nil {
return nil, 404, errors.New("item not found")
}
return item, 0, nil // 0 = default 200
}
func createItem(ctx *gin.Context, arg *CreateReq) (*Item, int, error) {
item := &Item{Name: arg.Name}
return item, 201, nil // explicit 201 Created
}
func errorMsg(ctx *gin.Context, err error) *ErrResp {
return &ErrResp{Message: err.Error()}
}
Features:
- ✅ Context access
- ✅ Custom HTTP status code (200/201/400/404/500)
- ✅ Status code 0 falls back to default (200 success, 400 errors)
- ✅ Success and errors use different response types
Advanced Features
Custom Status Codes
Control HTTP status codes across different scenarios:
// Use 400 on param/logic errors
status := warpginhandle.NewStatus400()
handler := warpginhandle.HandleC1(processFunc, bindFunc, makeResp, status)
// Use 200 across responses (error info in JSON)
status := warpginhandle.NewStatus200()
handler := warpginhandle.HandleC1(processFunc, bindFunc, makeResp, status)
Custom Parameter Binding
Create custom binding logic:
func customBind[ARG any](ctx *gin.Context) (*ARG, error) {
var arg ARG
// Custom binding logic
if err := ctx.ShouldBindHeader(&arg); err != nil {
return nil, err
}
return &arg, nil
}
engine.POST("/api/process", warpginhandle.C1(handleFunc, customBind, makeResp))
Service Registration Helpers
Organize routes with service interfaces:
// regginservice package provides clean registration
type UserService struct{}
func (s *UserService) RegisterRoutes(group *gin.RouterGroup) {
group.GET("/profile", s.GetProfile)
group.POST("/update", s.UpdateProfile)
}
// Service registration
regginservice.SetRouteGroup(engine, "v1/users", &UserService{})
API Reference
Core Package (reggin)
// Route definition with generic response type
type Route[RES any] struct {
Method Method // HTTP method (GET, POST, etc)
Path string // Route path
Handle RequestHandlerFunc[RES] // Handler function
}
// Application interface for service organization
type Application[RES any] interface {
GetRoutes() Routes[RES]
}
// Register routes to RouterGroup
func PackageRoutes[RES any](group *gin.RouterGroup, app Application[RES])
func RegisterRoutes[RES any](group *gin.RouterGroup, routes Routes[RES])
Wrapper Package (warpginhandle)
R Pattern Functions:
func RX[ARG, RES, RESPONSE any](run HandleR1Func[ARG, RES], errorMsg ErrorMsgFunc[RESPONSE]) gin.HandlerFunc
func R0[RES, RESPONSE any](run HandleR0Func[RES], errorMsg ErrorMsgFunc[RESPONSE]) gin.HandlerFunc
func R1[ARG, RES, RESPONSE any](run HandleR1Func[ARG, RES], parseReq ParseArgFunc[ARG], errorMsg ErrorMsgFunc[RESPONSE]) gin.HandlerFunc
C Pattern Functions:
func CX[ARG, RES, RESPONSE any](run HandleC1Func[ARG, RES], makeResp MakeRespFunc[RES, RESPONSE]) gin.HandlerFunc
func C0[RES, RESPONSE any](run HandleC0Func[RES], makeResp MakeRespFunc[RES, RESPONSE]) gin.HandlerFunc
func C1[ARG, RES, RESPONSE any](run HandleC1Func[ARG, RES], parseReq ParseArgFunc[ARG], makeResp MakeRespFunc[RES, RESPONSE]) gin.HandlerFunc
P Pattern Functions:
func PX[ARG, RES, RESPONSE any](run HandleP1Func[ARG, RES], makeResp MakeRespFunc[RES, RESPONSE]) gin.HandlerFunc
func P0[RES, RESPONSE any](run HandleP0Func[RES], makeResp MakeRespFunc[RES, RESPONSE]) gin.HandlerFunc
func P1[ARG, RES, RESPONSE any](run HandleP1Func[ARG, RES], parseReq ParseArgFunc[ARG], makeResp MakeRespFunc[RES, RESPONSE]) gin.HandlerFunc
F Pattern Functions:
func FX[ARG, RES, RESPONSE any](run HandleF1Func[ARG, RES], errorMsg ErrorMsgFunc[RESPONSE]) gin.HandlerFunc
func F0[RES, RESPONSE any](run HandleF0Func[RES], errorMsg ErrorMsgFunc[RESPONSE]) gin.HandlerFunc
func F1[ARG, RES, RESPONSE any](run HandleF1Func[ARG, RES], parseReq ParseArgFunc[ARG], errorMsg ErrorMsgFunc[RESPONSE]) gin.HandlerFunc
H Pattern Functions:
func HX[ARG, RES, RESPONSE any](run HandleH1Func[ARG, RES], errorMsg ErrorMsgFunc[RESPONSE]) gin.HandlerFunc
func H0[RES, RESPONSE any](run HandleH0Func[RES], errorMsg ErrorMsgFunc[RESPONSE]) gin.HandlerFunc
func H1[ARG, RES, RESPONSE any](run HandleH1Func[ARG, RES], parseReq ParseArgFunc[ARG], errorMsg ErrorMsgFunc[RESPONSE]) gin.HandlerFunc
Binding Functions:
func BIND[ARG any](ctx *gin.Context) (*ARG, error) // JSON binding (long name)
func B[ARG any](ctx *gin.Context) (*ARG, error) // JSON binding (short name)
func Q[ARG any](ctx *gin.Context) (*ARG, error) // Query param binding
func QueryJson[ARG any](ctx *gin.Context) (*ARG, error) // Query with JSON tags
Service Package (regginservice)
// Engine-level service registration
type EndpointHandler interface {
RegisterRoutes(engine *gin.Engine)
}
func AddEndpoints(engine *gin.Engine, service EndpointHandler)
func SetupService(engine *gin.Engine, service EndpointHandler)
// RouterGroup-level service registration
type RouteGroupHandler interface {
RegisterRoutes(group *gin.RouterGroup)
}
func AddRouteGroup(group *gin.RouterGroup, routeGroup RouteGroupHandler)
func SetRouteGroup(engine *gin.Engine, relativePath string, routeGroup RouteGroupHandler)
// IRouter-level service registration
type IRouterHandler interface {
RegisterRoutes(router gin.IRouter)
}
func AddRoutes(router gin.IRouter, handler IRouterHandler)
func SetRoutes(router gin.IRouter, relativePath string, handler IRouterHandler)
Best Practices
1. Choose the Right Pattern
- Use C pattern in most features - it provides comprehensive control and team consistency
- Use F pattern in rapid prototyping when you need quick results
- Use P pattern when unit testing pure business logic is important
- Use H pattern in REST APIs needing custom HTTP status codes (400/404/500)
- Use R pattern in simple utilities without context needs
Define response format once and reuse:
type Response struct {
Code int `json:"code"`
Desc string `json:"desc"`
Data any `json:"data"`
}
func makeResp[RES any](ctx *gin.Context, res *RES, err error) *Response {
if err != nil {
return &Response{Code: -1, Desc: err.Error(), Data: nil}
}
return &Response{Code: 0, Desc: "SUCCESS", Data: res}
}
// Use in all routes
engine.POST("/api/a", warpginhandle.CX(handleA, makeResp[ResultA]))
engine.POST("/api/b", warpginhandle.CX(handleB, makeResp[ResultB]))
3. Service Organization
Organize routes into distinct services:
// user_service.go
type UserService struct{}
func (s *UserService) GetRoutes() reggin.Routes[Response] { ... }
// order_service.go
type OrderService struct{}
func (s *OrderService) GetRoutes() reggin.Routes[Response] { ... }
// main.go
reggin.PackageRoutes[Response](engine.Group("v1/users"), &UserService{})
reggin.PackageRoutes[Response](engine.Group("v1/orders"), &OrderService{})
4. Effective Failure Handling
Use the erero package to enhance context when issues occur:
import "github.com/yylego/erero"
func handleRequest(ctx *gin.Context, arg *RequestArg) (*ResultType, error) {
data, err := database.Query(arg.ID)
if err != nil {
return nil, erero.Wro(err) // Add context and location info to failures
}
return processData(data)
}
Design Principles
reggin is built on these principles:
- Type Checking: Use Go generics to catch errors at compile time, not runtime
- Clean Code: Separate business logic from HTTP concerns
- Flexible Design: Multiple patterns to match different coding styles
- Simple Approach: Minimum boilerplate while maintaining Gin compatibility
- Reusable Components: Define once, use everywhere
Comparison with Standard Gin
Standard Gin:
engine.POST("/api/process", func(c *gin.Context) {
var req RequestType
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"code": -1, "desc": err.Error()})
return
}
result, err := processLogic(req)
if err != nil {
c.JSON(500, gin.H{"code": -1, "desc": err.Error()})
return
}
c.JSON(200, gin.H{"code": 0, "data": result})
})
With reggin:
engine.POST("/api/process", warpginhandle.CX(processLogic, makeResp[ResultType]))
func processLogic(ctx *gin.Context, req *RequestType) (*ResultType, error) {
return processData(req) // Just focus on business logic
}
📄 License
MIT License. See LICENSE.
🤝 Contributing
Contributions are welcome! Report bugs, suggest features, and contribute code:
- 🐛 Found a mistake? Open an issue on GitHub with reproduction steps
- 💡 Have a feature idea? Create an issue to discuss the suggestion
- 📖 Documentation confusing? Report it so we can improve
- 🚀 Need new features? Share the use cases to help us understand requirements
- ⚡ Performance issue? Help us optimize through reporting slow operations
- 🔧 Configuration problem? Ask questions about complex setups
- 📢 Follow project progress? Watch the repo to get new releases and features
- 🌟 Success stories? Share how this package improved the workflow
- 💬 Feedback? We welcome suggestions and comments
🔧 Development
New code contributions, follow this process:
- Fork: Fork the repo on GitHub (using the webpage UI).
- Clone: Clone the forked project (
git clone https://github.com/yourname/repo-name.git).
- Navigate: Navigate to the cloned project (
cd repo-name)
- Branch: Create a feature branch (
git checkout -b feature/xxx).
- Code: Implement the changes with comprehensive tests
- Testing: (Golang project) Ensure tests pass (
go test ./...) and follow Go code style conventions
- Documentation: Update documentation to support client-facing changes and use significant commit messages
- Stage: Stage changes (
git add .)
- Commit: Commit changes (
git commit -m "Add feature xxx") ensuring backward compatible code
- Push: Push to the branch (
git push origin feature/xxx).
- PR: Open a merge request on GitHub (on the GitHub webpage) with detailed description.
Please ensure tests pass and include relevant documentation updates.
🌟 Support
Welcome to contribute to this project via submitting merge requests and reporting issues.
Project Support:
- ⭐ Give GitHub stars if this project helps you
- 🤝 Share with teammates and (golang) programming friends
- 📝 Write tech blogs about development tools and workflows - we provide content writing support
- 🌟 Join the ecosystem - committed to supporting open source and the (golang) development scene
Have Fun Coding with this package! 🎉🎉🎉
GitHub Stars
