Skip to content

emmybxt/go-controller

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gocontroller

License: MIT

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/http compatibility 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:

  1. Classic interface style:
func (c *UserController) RegisterRoutes(r *gocontroller.RouteGroup) {
    r.GET("/users/:id", c.GetByID)
    r.POST("/users", c.Create, AuthMiddleware())
}
  1. 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, len
  • email, url, uri, hostname
  • uuid, uuid4, ip, ipv4, ipv6
  • oneof, startswith, endswith, contains
  • gt, gte, lt, lte
  • datetime
  • dive for 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 }

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) and ctx.Fail(status, msg) for envelope style

Router Improvements

  • Automatic 405 Method Not Allowed + Allow header 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) middleware
  • ContextValue[T](ctx, key) typed extraction
  • MustContextValue[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/DELETE
  • ParseDTO[T](ctx)
  • NewHTTPError(status, message)
  • ControllerMetadata, RouteMetadata
  • GET/POST/PUT/DELETE metadata helpers
  • RegisterGeneratedControllerMetadata (used by generated code)
  • Validator, ValidatorFunc, SetDefaultValidator, DefaultValidator
  • APIError, NewAPIError, helper constructors
  • RequestID(), 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:

  1. Update module path in go.mod to your GitHub repo.
  2. Add semantic tags (v0.1.0, v0.2.0, etc.).
  3. Add CI (go test ./..., go vet ./...).
  4. Add changelog and license.
  5. Add examples for both classic and annotation styles.

License

This project is licensed under the MIT License. See LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages