Skip to main content

Creating Controllers

Developer Beginner

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.Service interface 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 present
  • omitempty - Skip validation if empty
  • min=N - Minimum value/length
  • max=N - Maximum value/length
  • email - Valid email format
  • alphanum - Alphanumeric characters only
  • url - 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/products
  • POST /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 ✅

  1. Keep controllers thin - delegate to services
  2. Validate all input - use binding tags and custom validation
  3. Use proper HTTP status codes - 200, 201, 400, 404, 500
  4. Handle all errors - never ignore errors
  5. Use audit logging - track important operations
  6. Document your endpoints - add comments
  7. Use consistent naming - List, Get, Create, Update, Delete

DON'T ❌

  1. Don't put business logic in controllers - use services
  2. Don't access database directly - use service layer
  3. Don't ignore validation - always validate input
  4. Don't return detailed errors - sanitize error messages
  5. Don't use panic - return errors properly

Next Steps


Need help with controllers? Check the demo or ask in GitHub Discussions.