Controllers
Controllers are crucial components in the request lifecycle, serving as the orchestrators between incoming requests and your application’s business logic. Different types of controllers are designed to handle specific types of requests and response formats. Based on gFly project structure, which includes dedicated directories for API
and Page
controllers, let’s explore the various controller types and their characteristics.
Instead of defining all of your request handling logic as closures in your route files, you may wish to organize this behavior using the implementation of IHandler
interface.
// IHandler Interface for a handler request.
type IHandler interface {
// Validate validates the request context.
//
// Parameters:
// - c (*Ctx): The current HTTP request context.
//
// Returns:
// - error: An error if validation fails, otherwise nil.
Validate(c *Ctx) error
// Handle handles the request context.
//
// Parameters:
// - c (*Ctx): The current HTTP request context.
//
// Returns:
// - error: An error if handling fails, otherwise nil.
Handle(c *Ctx) error
}
Each controller only handles a certain task, and gFly encourages a controller file to handle only a certain logic.
- RestAPI directory
app/http/controllers/api
- Webpage directory
app/http/controllers/page
For example, the CreateUserApi controller should only contain code for handling new user creations. You can create many different controllers for each business requirement. This will be simple in file structure and content inside. By default, controllers are stored in the:
Let analyze a controller signin_api.go
package api
import (
"gfly/app/constants"
"gfly/app/http"
"gfly/app/http/response"
"gfly/app/modules/auth"
"gfly/app/modules/auth/dto"
"gfly/app/modules/auth/request"
"gfly/app/modules/auth/services"
"gfly/app/modules/auth/transformers"
"github.com/gflydev/core"
)
// ====================================================================
// ======================== Controller Creation =======================
// ====================================================================
type SignInApi struct {
core.Api
}
// NewSignInApi is a constructor
func NewSignInApi() *SignInApi {
return &SignInApi{}
}
// ====================================================================
// ======================== Request Validation ========================
// ====================================================================
// Validate data from request
func (h *SignInApi) Validate(c *core.Ctx) error {
return http.ProcessRequest[request.SignIn, dto.SignIn](c)
}
// ====================================================================
// ========================= Request Handling =========================
// ====================================================================
// Handle func handle sign in user then returns access token and refresh token
// @Description Authenticating user's credentials then return access and refresh token if valid. Otherwise, return an error message.
// @Summary authenticating user's credentials
// @Tags Auth
// @Accept json
// @Produce json
// @Param data body request.SignIn true "Signin payload"
// @Success 200 {object} response.SignIn
// @Failure 400 {object} response.Error
// @Router /auth/signin [post]
func (h *SignInApi) Handle(c *core.Ctx) error {
// Get valid data from context
signInDto := c.GetData(constants.Data).(dto.SignIn)
// Perform login logic and check error
tokens, err := services.SignIn(signInDto)
if err != nil {
return c.Error(response.Error{
Code: core.StatusBadRequest,
Message: err.Error(),
})
}
// Transform data and response
return c.JSON(transformers.ToSignInResponse(tokens))
}
gFly creates 2 structs core.Api
for API and core.Page
for Webpage. You should use it for your controller
type SignInApi struct {
core.Api
}
Separate data validation into a separate function to make different processing needs clearer. Reduces overload and bloat in main logic processing.
// Validate Verify data from request.
func (h *SignInApi) Validate(c *core.Ctx) error {
// Receive request data
var requestBody request.SignIn
if errData := c.ParseBody(&requestBody); errData != nil {
return c.Error(errData)
}
// Convert to DTO
requestDto := requestBody.ToDto()
// Validate DTO
if errData := validation.Check(requestDto); errData != nil {
return c.Error(errData)
}
// Store data into context
c.SetData(constants.Data, requestDto)
return nil
}
Try to parse the body request and set it to request.SignIn
. The request is only simple username
and password
so we convert to DTO dto.SignIn
and validate the data. After the data has passed the inspection. Stores valid data into the context for use in the controller handler c.SetData(constants.Data, requestDto)
If the form request contains a lot of data and complex structure. You can separate the data and save it separately in c.SetData()
NotegFly provides a utility for converting data from Request to DTO. So you can use it to reduce code cost.
// Validate data from request func (h *SignInApi) Validate(c *core.Ctx) error { return http.ProcessRequest[request.SignIn, dto.SignIn](c) }
// SignIn struct to describe login user.
type SignIn struct {
Username string `json:"username" validate:"required,email,lte=255"`
Password string `json:"password" validate:"required,gte=6"`
}
Handle the logic of the request. The controller will separate the data into corresponding business-logic DTOs. From there, call the request processing logic in the service
layer.
Summarize the data obtained from the executed service
. Change to suit different display needs.
Depending on the display needs in the view
section, a transformer
handler can be added to transform.
func (h *SignInApi) Handle(c *core.Ctx) error {
// Get valid data from context
signInDto := c.GetData(constants.Data).(dto.SignIn)
// Perform login logic and check error
tokens, err := services.SignIn(signInDto)
if err != nil {
return c.Error(response.Error{
Code: core.StatusBadRequest,
Message: err.Error(),
})
}
// Transform data and response
return c.JSON(transformers.ToSignInResponse(tokens))
}
Top notes for the creation of the API document (Swagger). There will be a detailed description of the API group, data types, and accepted methods. And the return statuses.
Start by taking the data signInDto := c.GetData(constants.Data).(dto.SignIn)
and converting it to the appropriate dto.SignIn
type.
The logic that handles sign-in is contained entirely within services.SignIn
. If there are no errors, the data returned to the user will be of type response.SignIn
. If an error occurs, response.Error
will be returned. Both types of returned data correspond to the declaration on Swagger.
We do not recommend keeping these important processes in the controller. Let’s put the processing into the service
layer. There you will comfortably do more things. Keep controller
as simple as possible.
Data passed from the controller
call via service
should be through literals, models, DTOs. Absolutely should not be a Form Request or Context Request object.
Let’s take a simple example with logic that handles user registration. And the main processing includes 2 steps: saving data to the user table and sending mail.
In case 1, you put all the logic in the controller. What if one day the boss says the system needs to interact with the outside world and needs to perform user registration via command. There will read the CSV/Excel file. You may have to add a new registration logic that is exactly the same as what is already available in the Register controller. However if you had a service
that handled user registration then I’m sure you would reuse it.
Case 2. If you only pass logic to service
but the data to service
is Form Request or Context Request object, then the bad thing is not less. Because these two objects are only related to HTTP requests. The question is with Command/Schedule/… how can I create an HTTP request. So, the data needed for a service
should be data of literals, models, DTOs
If your code is ready for the above two cases, then you are completely ready for any future functional requirements.