Creating Controllers
Controllers are the entry point for HTTP requests in EZ-Console. This guide shows you how to create and configure controllers to handle API endpoints.
Controller Basics
A controller in EZ-Console:
- Handles HTTP requests and responses
- Validates input parameters
- Calls service layer for business logic
- Formats responses using utility functions
- Registers routes with the framework
Creating Your First Controller
Step 1: Define the Controller Struct
package controller
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"github.com/sven-victor/ez-console/server"
"github.com/sven-victor/ez-console/pkg/util"
)
type ProductController struct {
svc server.Service
}
func NewProductController(svc server.Service) *ProductController {
return &ProductController{svc: svc}
}
Key points:
- Store
server.Serviceinterface for accessing framework services - Use constructor function
New...Controller()for initialization
Step 2: Implement HTTP Handlers
func (c *ProductController) ListProducts(ctx *gin.Context) {
// Extract query parameters
search := ctx.Query("search")
page := ctx.DefaultQuery("page", "1")
// Call service layer
products, total, err := c.svc.Product().List(ctx.Request.Context(), search, page)
if err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E5001", "Failed to list products", err))
return
}
// Return response
util.RespondWithSuccessList(ctx, http.StatusOK, products, total, page, 10)
}
func (c *ProductController) GetProduct(ctx *gin.Context) {
// Extract path parameter
id := ctx.Param("id")
if id == "" {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Product ID is required"))
return
}
// Call service layer
product, err := c.svc.Product().GetByID(ctx.Request.Context(), id)
if err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E5001", "Failed to get product", err))
return
}
// Return response
util.RespondWithSuccess(ctx, http.StatusOK, product)
}
func (c *ProductController) CreateProduct(ctx *gin.Context) {
var req CreateProductRequest
// Bind and validate request body
if err := ctx.ShouldBindJSON(&req); err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Invalid request", err))
return
}
// Call service layer
product, err := c.svc.Product().Create(ctx.Request.Context(), req)
if err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E5001", "Failed to create product", err))
return
}
// Return response
util.RespondWithSuccess(ctx, http.StatusCreated, product)
}
Step 3: Register Routes
func (c *ProductController) RegisterRoutes(ctx context.Context, router *gin.RouterGroup) {
products := router.Group("/products")
{
products.GET("", c.ListProducts)
products.GET("/:id", c.GetProduct)
products.POST("", c.CreateProduct)
products.PUT("/:id", c.UpdateProduct)
products.DELETE("/:id", c.DeleteProduct)
}
}
Step 4: Register with Framework
func init() {
server.RegisterControllers(func(ctx context.Context, svc server.Service) server.Controller {
return NewProductController(svc)
})
}
Request Parameter Handling
Path Parameters
Extract from URL path:
// Route: /products/:id
func (c *ProductController) GetProduct(ctx *gin.Context) {
id := ctx.Param("id") // Get :id from URL
// Validate
if id == "" {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "ID is required"))
return
}
// Use id...
}
Query Parameters
Extract from query string:
// Route: /products?search=laptop&page=1&page_size=10
func (c *ProductController) ListProducts(ctx *gin.Context) {
search := ctx.Query("search") // Get search query
page := ctx.DefaultQuery("page", "1") // With default value
pageSize := ctx.DefaultQuery("page_size", "10")
// Convert to int if needed
pageInt, _ := strconv.Atoi(page)
// Use parameters...
}
Request Body (JSON)
Parse JSON request body:
type CreateProductRequest struct {
Name string `json:"name" binding:"required,min=1,max=128"`
Description string `json:"description" binding:"max=500"`
Price float64 `json:"price" binding:"required,min=0"`
Stock int `json:"stock" binding:"min=0"`
}
func (c *ProductController) CreateProduct(ctx *gin.Context) {
var req CreateProductRequest
// Bind and validate
if err := ctx.ShouldBindJSON(&req); err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Invalid request", err))
return
}
// Use req...
}
Input Validation
Using Binding Tags
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=32,alphanum"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8,max=128"`
Age int `json:"age" binding:"omitempty,min=18,max=120"`
}
Common validation tags:
required- Field must be presentomitempty- Skip validation if emptymin=N- Minimum value/lengthmax=N- Maximum value/lengthemail- Valid email formatalphanum- Alphanumeric characters onlyurl- Valid URL format
Custom Validation
func (c *ProductController) CreateProduct(ctx *gin.Context) {
var req CreateProductRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Invalid request", err))
return
}
// Custom validation
if req.Price < 0 {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Price must be positive"))
return
}
if req.Stock < 0 {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Stock cannot be negative"))
return
}
// Continue...
}
Response Formatting
Success Response (Single Item)
util.RespondWithSuccess(ctx, http.StatusOK, product)
Response:
{
"code": "0",
"data": {
"id": "uuid",
"name": "Product Name",
"price": 99.99
}
}
Success Response (List)
util.RespondWithSuccessList(ctx, http.StatusOK, products, total, current, pageSize)
Response:
{
"code": "0",
"data": [...],
"total": 100,
"current": 1,
"page_size": 10
}
Error Response
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Invalid request"))
Response:
{
"code": "E4001",
"err": "Invalid request"
}
Route Grouping
Simple Grouping
func (c *ProductController) RegisterRoutes(ctx context.Context, router *gin.RouterGroup) {
products := router.Group("/products")
{
products.GET("", c.ListProducts)
products.POST("", c.CreateProduct)
}
}
Routes:
GET /api/productsPOST /api/products
Nested Grouping
func (c *ProductController) RegisterRoutes(ctx context.Context, router *gin.RouterGroup) {
products := router.Group("/products")
{
// Public routes
products.GET("", c.ListProducts)
products.GET("/:id", c.GetProduct)
// Protected routes
admin := products.Group("")
admin.Use(middleware.RequirePermission("products:write"))
{
admin.POST("", c.CreateProduct)
admin.PUT("/:id", c.UpdateProduct)
admin.DELETE("/:id", c.DeleteProduct)
}
}
}
Middleware Usage
Apply to All Routes
func (c *ProductController) RegisterRoutes(ctx context.Context, router *gin.RouterGroup) {
products := router.Group("/products")
products.Use(middleware.RequireAuth()) // All routes need auth
{
products.GET("", c.ListProducts)
products.POST("", c.CreateProduct)
}
}
Apply to Specific Routes
func (c *ProductController) RegisterRoutes(ctx context.Context, router *gin.RouterGroup) {
products := router.Group("/products")
{
// Public
products.GET("", c.ListProducts)
// Protected
products.POST("",
middleware.RequireAuth(),
middleware.RequirePermission("products:create"),
c.CreateProduct,
)
}
}
Complete Controller Example
package controller
import (
"context"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/sven-victor/ez-console/server"
"github.com/sven-victor/ez-console/pkg/util"
"github.com/sven-victor/ez-console/pkg/middleware"
)
// Request DTOs
type CreateProductRequest struct {
Name string `json:"name" binding:"required,min=1,max=128"`
Description string `json:"description" binding:"max=500"`
Price float64 `json:"price" binding:"required,min=0"`
Stock int `json:"stock" binding:"min=0"`
}
type UpdateProductRequest struct {
Name *string `json:"name" binding:"omitempty,min=1,max=128"`
Description *string `json:"description" binding:"omitempty,max=500"`
Price *float64 `json:"price" binding:"omitempty,min=0"`
Stock *int `json:"stock" binding:"omitempty,min=0"`
}
// Controller
type ProductController struct {
svc server.Service
}
func NewProductController(svc server.Service) *ProductController {
return &ProductController{svc: svc}
}
// List products
func (c *ProductController) ListProducts(ctx *gin.Context) {
search := ctx.Query("search")
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(ctx.DefaultQuery("page_size", "10"))
products, total, err := c.svc.Product().List(ctx.Request.Context(), search, page, pageSize)
if err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E5001", "Failed to list products", err))
return
}
util.RespondWithSuccessList(ctx, http.StatusOK, products, total, page, pageSize)
}
// Get single product
func (c *ProductController) GetProduct(ctx *gin.Context) {
id := ctx.Param("id")
if id == "" {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Product ID is required"))
return
}
product, err := c.svc.Product().GetByID(ctx.Request.Context(), id)
if err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E5001", "Failed to get product", err))
return
}
util.RespondWithSuccess(ctx, http.StatusOK, product)
}
// Create product
func (c *ProductController) CreateProduct(ctx *gin.Context) {
var req CreateProductRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Invalid request", err))
return
}
// Use audit logging
var product *model.Product
err := c.svc.StartAudit(ctx, "", func(auditLog *model.AuditLog) error {
product, err = c.svc.Product().Create(ctx.Request.Context(), req)
auditLog.ResourceType = "product"
auditLog.ResourceID = product.ResourceID
auditLog.Action = "create"
return err
})
if err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E5001", "Failed to create product", err))
return
}
util.RespondWithSuccess(ctx, http.StatusCreated, product)
}
// Update product
func (c *ProductController) UpdateProduct(ctx *gin.Context) {
id := ctx.Param("id")
if id == "" {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Product ID is required"))
return
}
var req UpdateProductRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Invalid request", err))
return
}
var product *model.Product
err := c.svc.StartAudit(ctx, id, func(auditLog *model.AuditLog) error {
product, err = c.svc.Product().Update(ctx.Request.Context(), id, req)
auditLog.ResourceType = "product"
auditLog.Action = "update"
return err
})
if err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E5001", "Failed to update product", err))
return
}
util.RespondWithSuccess(ctx, http.StatusOK, product)
}
// Delete product
func (c *ProductController) DeleteProduct(ctx *gin.Context) {
id := ctx.Param("id")
if id == "" {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Product ID is required"))
return
}
err := c.svc.StartAudit(ctx, id, func(auditLog *model.AuditLog) error {
err := c.svc.Product().Delete(ctx.Request.Context(), id)
auditLog.ResourceType = "product"
auditLog.Action = "delete"
return err
})
if err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E5001", "Failed to delete product", err))
return
}
util.RespondWithSuccess(ctx, http.StatusOK, nil)
}
// Register routes
func (c *ProductController) RegisterRoutes(ctx context.Context, router *gin.RouterGroup) {
products := router.Group("/products")
{
// Public routes
products.GET("", c.ListProducts)
products.GET("/:id", c.GetProduct)
// Protected routes
products.POST("",
middleware.RequireAuth(),
middleware.RequirePermission("products:create"),
c.CreateProduct,
)
products.PUT("/:id",
middleware.RequireAuth(),
middleware.RequirePermission("products:update"),
c.UpdateProduct,
)
products.DELETE("/:id",
middleware.RequireAuth(),
middleware.RequirePermission("products:delete"),
c.DeleteProduct,
)
}
}
// Register with framework
func init() {
server.RegisterControllers(func(ctx context.Context, svc server.Service) server.Controller {
return NewProductController(svc)
})
}
Best Practices
DO ✅
- Keep controllers thin - delegate to services
- Validate all input - use binding tags and custom validation
- Use proper HTTP status codes - 200, 201, 400, 404, 500
- Handle all errors - never ignore errors
- Use audit logging - track important operations
- Document your endpoints - add comments
- Use consistent naming - List, Get, Create, Update, Delete
DON'T ❌
- Don't put business logic in controllers - use services
- Don't access database directly - use service layer
- Don't ignore validation - always validate input
- Don't return detailed errors - sanitize error messages
- Don't use panic - return errors properly
Next Steps
- Learn about business logic with services
- Understand request validation
- Implement error handling
- Add authentication
Need help with controllers? Check the demo or ask in GitHub Discussions.