Controllers
Organize related routes into controller structs
Controllers group related route handlers into a single struct, providing a clean way to organize routes by domain or resource. Controllers can declare a path prefix, middleware, tags, and dependencies, keeping your route registration modular and testable.
Controller Interface
At minimum, a controller must implement the Controller interface with two methods.
type Controller interface {
// Name returns the controller identifier
Name() string
// Routes registers routes on the router
Routes(r forge.Router) error
}The Name() method returns a unique identifier for the controller. The Routes() method receives a router (scoped to the controller's prefix if one is defined) and registers all the controller's routes.
Basic Controller
Here is a minimal controller that registers a few routes.
type HealthController struct{}
func (c *HealthController) Name() string {
return "health"
}
func (c *HealthController) Routes(r forge.Router) error {
r.GET("/health", func(ctx forge.Context) error {
return ctx.JSON(200, map[string]string{"status": "ok"})
})
r.GET("/readiness", func(ctx forge.Context) error {
return ctx.JSON(200, map[string]string{"ready": "true"})
})
return nil
}Registering Controllers
Controllers are registered on the router (or an App) using RegisterController.
app.RegisterController(&HealthController{})
app.RegisterController(&UserController{store: userStore})
app.RegisterController(&OrderController{store: orderStore})The router calls the controller's Routes method during registration, passing a sub-router scoped to the controller's prefix (if one is provided via ControllerWithPrefix).
Optional Interfaces
Controllers can implement additional interfaces to configure prefix, middleware, tags, and dependencies.
ControllerWithPrefix
Set a path prefix for all routes in the controller. The prefix is prepended to every route path registered in Routes.
type ControllerWithPrefix interface {
Controller
Prefix() string
}type UserController struct {
store *UserStore
}
func (c *UserController) Name() string { return "users" }
func (c *UserController) Prefix() string { return "/api/v1/users" }
func (c *UserController) Routes(r forge.Router) error {
// These routes become /api/v1/users and /api/v1/users/:id
r.GET("/", c.list)
r.POST("/", c.create)
r.GET("/:id", c.get)
r.PUT("/:id", c.update)
r.DELETE("/:id", c.delete)
return nil
}ControllerWithMiddleware
Apply middleware to all routes in the controller.
type ControllerWithMiddleware interface {
Controller
Middleware() []forge.Middleware
}func (c *UserController) Middleware() []forge.Middleware {
return []forge.Middleware{
middleware.Logging(c.logger),
middleware.RateLimit(c.limiter, c.logger),
}
}ControllerWithTags
Add tags to all routes in the controller. Tags appear in OpenAPI documentation and can be used for route lookup with RoutesByTag.
type ControllerWithTags interface {
Controller
Tags() []string
}func (c *UserController) Tags() []string {
return []string{"users", "v1"}
}ControllerWithDependencies
Declare dependencies on other controllers for ordering. The router ensures that dependencies are registered before this controller.
type ControllerWithDependencies interface {
Controller
Dependencies() []string
}type OrderController struct {
store *OrderStore
}
func (c *OrderController) Name() string { return "orders" }
func (c *OrderController) Dependencies() []string { return []string{"users", "products"} }Complete CRUD Controller
Here is a full CRUD controller that implements all optional interfaces.
type ProductController struct {
store *ProductStore
logger forge.Logger
}
func NewProductController(store *ProductStore, logger forge.Logger) *ProductController {
return &ProductController{store: store, logger: logger}
}
func (c *ProductController) Name() string { return "products" }
func (c *ProductController) Prefix() string { return "/api/v1/products" }
func (c *ProductController) Tags() []string {
return []string{"products"}
}
func (c *ProductController) Middleware() []forge.Middleware {
return []forge.Middleware{
middleware.Logging(c.logger),
}
}
func (c *ProductController) Routes(r forge.Router) error {
// List products
r.GET("/", c.list,
forge.WithName("list-products"),
forge.WithSummary("List all products"),
forge.WithResponseSchema(200, "Product list", &[]ProductResponse{}),
)
// Get a product
r.GET("/:id", c.get,
forge.WithName("get-product"),
forge.WithSummary("Get a product by ID"),
forge.WithResponseSchema(200, "Product details", &ProductResponse{}),
forge.WithResponseSchema(404, "Product not found", &ErrorResponse{}),
)
// Create a product (requires auth)
r.POST("/", c.create,
forge.WithName("create-product"),
forge.WithSummary("Create a new product"),
forge.WithAuth("jwt"),
forge.WithRequestSchema(&CreateProductRequest{}),
forge.WithResponseSchema(201, "Product created", &ProductResponse{}),
forge.WithValidation(true),
)
// Update a product
r.PUT("/:id", c.update,
forge.WithName("update-product"),
forge.WithSummary("Update an existing product"),
forge.WithAuth("jwt"),
forge.WithRequestSchema(&UpdateProductRequest{}),
forge.WithResponseSchema(200, "Product updated", &ProductResponse{}),
)
// Delete a product
r.DELETE("/:id", c.delete,
forge.WithName("delete-product"),
forge.WithSummary("Delete a product"),
forge.WithAuth("jwt"),
forge.WithRequiredAuth("jwt", "admin"),
)
return nil
}
// Handler methods
func (c *ProductController) list(ctx forge.Context) error {
products, err := c.store.List(ctx.Context())
if err != nil {
return forge.InternalError(err)
}
return ctx.JSON(200, products)
}
func (c *ProductController) get(ctx forge.Context) error {
id := ctx.Param("id")
product, err := c.store.GetByID(ctx.Context(), id)
if err != nil {
return forge.NotFound("product not found")
}
return ctx.JSON(200, product)
}
func (c *ProductController) create(ctx forge.Context, req *CreateProductRequest) (*ProductResponse, error) {
product, err := c.store.Create(ctx.Context(), req)
if err != nil {
return nil, err
}
return &ProductResponse{
ID: product.ID,
Name: product.Name,
Price: product.Price,
}, nil
}
func (c *ProductController) update(ctx forge.Context, req *UpdateProductRequest) (*ProductResponse, error) {
id := ctx.Param("id")
product, err := c.store.Update(ctx.Context(), id, req)
if err != nil {
return nil, err
}
return &ProductResponse{
ID: product.ID,
Name: product.Name,
Price: product.Price,
}, nil
}
func (c *ProductController) delete(ctx forge.Context) error {
id := ctx.Param("id")
if err := c.store.Delete(ctx.Context(), id); err != nil {
return forge.InternalError(err)
}
return ctx.NoContent(204)
}Controller with Dependency Injection
Controllers integrate naturally with Forge's dependency injection. Register services in the DI container and pass them to controllers during construction.
func main() {
app := forge.New()
// Register services in the DI container
app.Register("userStore", &UserStore{db: db})
app.Register("productStore", &ProductStore{db: db})
// Create controllers with injected dependencies
userStore, _ := app.Resolve("userStore")
productStore, _ := app.Resolve("productStore")
app.RegisterController(NewUserController(userStore.(*UserStore), app.Logger()))
app.RegisterController(NewProductController(productStore.(*ProductStore), app.Logger()))
app.Start(context.Background())
}Alternatively, use the combined handler pattern to let Forge resolve services automatically:
func (c *ProductController) Routes(r forge.Router) error {
// Forge auto-resolves *ProductStore from the DI container
r.POST("/", func(ctx forge.Context, store *ProductStore, req *CreateProductRequest) (*ProductResponse, error) {
return store.Create(ctx.Context(), req)
})
return nil
}Organizing Controllers
For larger applications, organize controllers by domain in separate packages.
internal/
controllers/
users.go // UserController
products.go // ProductController
orders.go // OrderController
health.go // HealthController
store/
users.go
products.go
orders.goRegister all controllers at the application entry point:
func setupControllers(app forge.Router, deps *Dependencies) {
app.RegisterController(controllers.NewHealthController())
app.RegisterController(controllers.NewUserController(deps.UserStore, deps.Logger))
app.RegisterController(controllers.NewProductController(deps.ProductStore, deps.Logger))
app.RegisterController(controllers.NewOrderController(deps.OrderStore, deps.Logger))
}How is this guide?