README
¶
gocontroller
gocontroller is a lightweight Go library for building APIs with a familiar controller/module pattern inspired by NestJS and Express, while staying idiomatic Go.
It gives you:
- Controller-oriented route organization
- Route, group, and module middleware composition
- Nest-style module graph (
imports,providers,controllers) - Reflection-based dependency injection
- DTO-style request parsing and validation
- Annotation-driven route metadata with
go generate net/httpcompatibility so you can mount into Gin, Echo, Fiber adapters
Why this library exists
Go frameworks are powerful, but many teams want a predictable architecture where:
- routes are declared near controller methods
- dependencies are constructor-injected
- feature modules are explicit
- request DTOs are validated consistently
gocontroller focuses on architecture and composition so your business code stays clean.
Installation
go get github.com/emmybxt/go-controller
Quick Start (5 minutes)
package main
import (
"net/http"
"github.com/emmybxt/go-controller/gocontroller"
)
type HealthController struct{}
func (c *HealthController) RegisterRoutes(r *gocontroller.RouteGroup) {
r.GET("/health", c.Health)
}
func (c *HealthController) Health(ctx *gocontroller.Context) error {
return ctx.JSON(http.StatusOK, map[string]string{"status": "ok"})
}
func NewHealthController() *HealthController { return &HealthController{} }
func main() {
app, err := gocontroller.NewApp(&gocontroller.Module{
Name: "AppModule",
Prefix: "/api",
Controllers: []any{NewHealthController},
})
if err != nil {
panic(err)
}
_ = app.Listen(":8080")
}
Core Concepts
1) Controllers
Two supported styles:
- Classic interface style:
func (c *UserController) RegisterRoutes(r *gocontroller.RouteGroup) {
r.GET("/users/:id", c.GetByID)
r.POST("/users", c.Create, AuthMiddleware())
}
- Metadata style:
func (c *UserController) ControllerMetadata() gocontroller.ControllerMetadata {
return gocontroller.ControllerMetadata{
Prefix: "/users",
Routes: []gocontroller.RouteMetadata{
gocontroller.GET("/:id", "GetByID"),
gocontroller.POST("/", "Create", AuthMiddleware()),
},
}
}
2) Modules
Modules let you group providers/controllers and compose feature boundaries.
userModule := &gocontroller.Module{
Name: "UserModule",
Prefix: "/users",
Providers: []any{NewUserService, NewUserRepo},
Controllers: []any{NewUserController},
}
appModule := &gocontroller.Module{
Name: "AppModule",
Prefix: "/api",
Imports: []*gocontroller.Module{userModule},
}
3) Dependency Injection
Register providers as:
- concrete instances
- constructor functions returning
T - constructor functions returning
(T, error)
Dependencies are resolved recursively from constructor parameters.
func NewUserService(repo *UserRepo) *UserService { ... }
func NewUserController(svc *UserService) *UserController { ... }
4) DTO Validation
Use ParseDTO[T] or ctx.BindJSON(&dto).
type CreateUserDTO struct {
Name string `json:"name" validate:"required,min=2,max=50"`
Email string `json:"email" validate:"required,email"`
}
func (c *UserController) Create(ctx *gocontroller.Context) error {
dto, err := gocontroller.ParseDTO[CreateUserDTO](ctx)
if err != nil {
return err // handled as 400 when validation fails
}
return ctx.JSON(http.StatusCreated, dto)
}
Validation is powered by go-playground/validator/v10, so you can use its broad built-in tag set (for the pinned version in this module), including tags like:
required,min,max,lenemail,url,uri,hostnameuuid,uuid4,ip,ipv4,ipv6oneof,startswith,endswith,containsgt,gte,lt,ltedatetimedivefor slices/maps
You can combine tags exactly as in validator syntax, e.g. validate:"required,oneof=admin user,lowercase".
4.1) Pluggable Validator Engine
Validation is fully swappable via gocontroller.Validator.
type Validator interface {
Validate(any) error
}
Per-app override:
app.SetValidator(myValidator)
Global default override:
gocontroller.SetDefaultValidator(myValidator)
Function adapter:
app.SetValidator(gocontroller.ValidatorFunc(func(v any) error {
// call ozzo/json-schema/custom rules
return nil
}))
Default engine is go-playground/validator/v10 wrapped by NewGoPlaygroundValidator().
5) Middleware
Attach middleware at multiple levels:
- app/router (global)
- module
- route group
- route
r.POST("/users", c.Create, AuthMiddleware(), AuditMiddleware())
Middleware signature:
type Middleware func(HandlerFunc) HandlerFunc
Built-in helpers:
gocontroller.RequestLogger()gocontroller.AdaptHTTPMiddleware(func(http.Handler) http.Handler)gocontroller.RequestID()gocontroller.Recovery(gocontroller.RecoveryConfig{...})gocontroller.CORS(gocontroller.CORSConfig{...})gocontroller.SecurityHeaders()gocontroller.RequireContextValue(key, "Unauthorized")
Annotation + Codegen (Decorator-like)
If you prefer Nest-like annotations, use comments + generator.
Step 1: Annotate
// @Controller("/users")
type UserController struct{}
// @Get("/:id")
func (c *UserController) GetByID(ctx *gocontroller.Context) error { return nil }
// @Post("/")
// @Use(AuthMiddleware())
func (c *UserController) Create(ctx *gocontroller.Context) error { return nil }
// @Patch("/:id")
func (c *UserController) Patch(ctx *gocontroller.Context) error { return nil }
The generator supports @Get, @Post, @Put, @Patch, @Delete, and
@Options. Controller methods may be split across files; generation discovers
the controller types first, then collects and deterministically orders every
annotated method. Static routes are registered before parameter and wildcard
routes so /workspace/stats cannot be shadowed by /:id/stats.
Step 2: Add go:generate
//go:generate go run ../cmd/gocontroller-gen -dir . -out zz_gocontroller_routes.gen.go
Step 3: Generate
go generate ./example
Generated metadata is auto-registered through init() and picked up by the module loader.
Avoid "forgot to generate" in deployments
go build does not run go generate automatically in Go.
Use build wrappers that always generate first:
make build # runs go generate ./... then go build ./...
make test # runs go generate ./... then go test ./...
And enforce freshness in CI:
make verify-generated
Framework Compatibility (Gin / Echo / Fiber)
Yes, it can be used with those frameworks.
gocontroller exposes App.Handler() http.Handler, so you can mount it where wrappers are available.
Gin
import "github.com/gin-gonic/gin"
ginEngine := gin.Default()
ginEngine.Any("/api/*any", gin.WrapH(app.Handler()))
Echo
import "github.com/labstack/echo/v4"
e := echo.New()
e.Any("/api/*", echo.WrapHandler(app.Handler()))
Fiber
import (
"github.com/gofiber/adaptor/v2"
"github.com/gofiber/fiber/v2"
)
f := fiber.New()
f.All("/api/*", adaptor.HTTPHandler(app.Handler()))
Notes:
- This keeps your controller/module architecture in one place.
- If you need deep native middleware/context features of each framework, use adapters selectively at the boundary.
Web + API Composition Helpers
You can avoid manual finalHandler path-switch logic with:
gocontroller.WebAPIHandler(webHandler, apiHandler, opts)gocontroller.NotFoundHTMLOrJSON(html404Path, jsonMessage)gocontroller.ServePage(publicDir, pageFile)
Example:
final := gocontroller.WebAPIHandler(webRouter, app.Handler(), gocontroller.HybridOptions{
WebExactPaths: []string{"/"},
WebPathPrefixes: []string{"/app", "/css/", "/js/"},
TreatSingleSegmentGETAsWeb: true,
})
Context Response Helpers
Built-in response shortcuts on *gocontroller.Context:
ctx.OK(data)ctx.Created(data)ctx.NoContent()ctx.BadRequest(msg)ctx.Unauthorized(msg)ctx.Forbidden(msg)ctx.NotFound(msg)ctx.Conflict(msg)ctx.InternalError(msg)ctx.Success(status, data)andctx.Fail(status, msg)for envelope style
Router Improvements
- Automatic
405 Method Not Allowed+Allowheader when path exists but method does not. - Wildcard routes with trailing
*.
router.GET("/assets/*", func(ctx *gocontroller.Context) error {
return ctx.OK(map[string]string{"path": ctx.Param("*")})
})
Standardized Errors
Use gocontroller.APIError for consistent API error responses:
return &gocontroller.APIError{
StatusCode: 422,
Code: "validation_failed",
Message: "Invalid input",
Details: map[string]any{"field": "email"},
}
Response shape:
{
"success": false,
"error": {
"code": "validation_failed",
"message": "Invalid input",
"details": {"field": "email"},
"trace_id": "..."
}
}
Built-in helpers:
gocontroller.NewAPIError(status, code, message)gocontroller.BadRequestError(...)gocontroller.UnauthorizedError(...)gocontroller.ForbiddenError(...)gocontroller.NotFoundError(...)gocontroller.ConflictError(...)gocontroller.InternalError(...)
You can also override global route error rendering:
app.SetErrorHandler(func(ctx *gocontroller.Context, err error) {
_ = ctx.JSON(418, map[string]any{"custom": true, "error": err.Error()})
})
Auth Context Helpers
Built-ins to reduce repeated auth glue:
RequireContextValue(key, message)middlewareContextValue[T](ctx, key)typed extractionMustContextValue[T](ctx, key, message)typed extraction with unauthorized error fallback
Graceful Runtime Helpers
You can run server lifecycle with context-aware graceful shutdown:
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
err := app.Run(ctx, gocontroller.ServerOptions{
Addr: ":8080",
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
ShutdownTimeout: 10 * time.Second,
})
API Surface
Main types/functions:
gocontroller.NewApp(*Module)(*App).Listen(addr)(*App).Handler()(*App).SetValidator(v)/(*App).Validator()(*App).Run(ctx, ServerOptions)(*App).NewHTTPServer(ServerOptions)(*App).SetErrorHandler(ErrorHandlerFunc)Module{ Name, Prefix, Providers, Controllers, Imports, Middleware }RouteGroup.GET/POST/PUT/DELETEParseDTO[T](ctx)NewHTTPError(status, message)ControllerMetadata,RouteMetadataGET/POST/PUT/DELETEmetadata helpersRegisterGeneratedControllerMetadata(used by generated code)Validator,ValidatorFunc,SetDefaultValidator,DefaultValidatorAPIError,NewAPIError, helper constructorsRequestID(),Recovery(RecoveryConfig{})CORS(CORSConfig{}),SecurityHeaders()RequireContextValue(...),ContextValue[T](...),MustContextValue[T](...)
Error Handling Behavior
Default behavior:
- route not found:
404 - validation error:
400 - explicit
NewHTTPError(...): mapped status - unknown handler error:
500
Public Library Checklist
Before publishing:
- Update module path in
go.modto your GitHub repo. - Add semantic tags (
v0.1.0,v0.2.0, etc.). - Add CI (
go test ./...,go vet ./...). - Add changelog and license.
- Add examples for both classic and annotation styles.
License
This project is licensed under the MIT License. See LICENSE.