Controllers #
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 a handler request.
type IHandler interface {
Validate(c *Ctx) error
Handle(c *Ctx) error
}
Each controller only handles a certain task, and gFly encourages a controller file to handle only a certain logic.
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:
RestAPI directory
app/http/controllers/api
Webpage directory
app/http/controllers/page
Let analyze a SignInApi
controller
type SignInApi struct {
gfly.Api
}
// Validate Verify data from request.
func (h *SignInApi) Validate(c *gfly.Ctx) error {
// Parse login form.
var signIn request.SignIn
err := c.ParseBody(&signIn)
if err != nil {
c.Status(gfly.StatusBadRequest)
return err
}
signInDto := signIn.ToDto()
// Validate login form.
if errorData, err := c.Validate(signInDto); err != nil {
_ = c.Error(errorData)
return err
}
// Store data in context.
c.SetData("data", signInDto)
return nil
}
// Handle Auth user and return access and refresh tokens.
// @Description Auth user and return access and refresh token.
// @Summary Auth user and return access and refresh token
// @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 *gfly.Ctx) error {
// Get valid data from context.
var signInDto = c.GetData("data").(dto.SignIn)
tokens, err := services.SignIn(&signInDto)
if err != nil {
return c.Error(response.Error{
Message: err.Error(),
Code: gfly.StatusBadRequest,
})
}
return c.Status(gfly.StatusOK).JSON(response.SignIn{
Access: tokens.Access,
Refresh: tokens.Refresh,
})
}
gFly creates 2 structs gfly.Api
for API and gfly.Page
for Webpage. You should use it for your controller
type SignInApi struct {
gfly.Api
}
Controller Validation #
Separate data validation into a separate function to make different processing needs clearer. Reduces overload and bloat in main logic processing.
Analyze the code: #
// Validate Verify data from request.
func (h *SignInApi) Validate(c *gfly.Ctx) error {
// Parse login form.
var signIn request.SignIn
err := c.ParseBody(&signIn)
if err != nil {
c.Status(gfly.StatusBadRequest)
return err
}
signInDto := signIn.ToDto()
// Validate login form.
if errorData, err := c.Validate(signInDto); err != nil {
_ = c.Error(errorData)
return err
}
// Store data in context.
c.SetData("data", signInDto)
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("data", signInDto)
If the form request contains a lot of data and complex structure. You can separate the data and save it separately in c.SetData()
Form request #
// 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"`
}
Controller Handling #
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.
Analyze the code: #
// Handle Auth user and return access and refresh tokens.
// @Description Auth user and return access and refresh token.
// @Summary Auth user and return access and refresh token
// @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 *gfly.Ctx) error {
// Get valid data from context.
var signInDto = c.GetData("data").(dto.SignIn)
tokens, err := services.SignIn(&signInDto)
if err != nil {
return c.Error(response.Error{
Message: err.Error(),
Code: gfly.StatusBadRequest,
})
}
return c.Status(gfly.StatusOK).JSON(response.SignIn{
Access: tokens.Access,
Refresh: tokens.Refresh,
})
}
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 var signInDto = c.GetData("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.
Request data #
How to get data via path
, request
(get
, post
, put
, delete
), upload
Path
parameter
#
router.GET("/user/{id}", page.NewGetUserAPI());
Get path parameter in controller
userIdStr := c.PathVal("id")
Query
parameter
#
Example below GET
request
curl -X GET http://localhost:7789/api/v1/users?name=john&age=10&sort=-first_name
Get data
// name=john
name := c.QueryStr("name")
// age=10
age, _ := c.QueryInt("age")
// sort=-first_name
sort := c.QueryStr("sort")
POST
parameter
#
Example below POST
request
curl -H "Content-Type: application/x-www-form-urlencoded" \
-d "first_name=John&last_name=Coi&age=39&email=john@mail.com" -v -X POST \
http://localhost:7789/api/v1/users
Get data
// first_name=John&last_name=Coi&age=39&email=john@mail.com
first_name := string(c.FormVal("first_name"))
last_name := string(c.FormVal("last_name"))
age, _ := c.FormInt("age")
email := string(c.FormVal("email"))
Post JSON
parameter
#
Example below POST
request
## Login
curl -X "POST" "http://localhost:7789/api/v1/auth/signin" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"username": "vinh@mail.com",
"password": "My$assWord"
}'
Get data
var signIn request.SignIn
_ := c.ParseBody(&signIn)
Upload
form
#
Form each single file
#
<form method="POST" action="/upload" enctype="multipart/form-data">
<input name="file1" type="file">
<input name="file2" type="file">
<button type="submit">Submit</button>
</form>
Controller
uploads, _ = c.FormUpload("file1", "file2")
for _, upload := range uploads {
fmt.Printf("File info %v \n", upload)
}
Form multi-file
#
<form method="POST" action="/upload" enctype="multipart/form-data">
<input name="file[]" type="file" multiple>
<button type="submit">Submit</button>
</form>
Controller
uploads, _ = c.FormUpload()
for _, upload := range uploads {
fmt.Printf("File info %v \n", upload)
}
Responding data #
How to response string
, HTML
, JSON
, download
data
Response error
#
// Simple
return errors.NotYetImplemented
// Structure JSON
return c.Error(response.Error{
Message: err.Error(),
Code: gfly.StatusBadRequest,
})
Response string
, HTML
#
// String
return c.String("Hello world")
// HTML
return c.HTML("<h2>Hello world</h2>")
Response JSON
#
return c.Status(gfly.StatusOK).JSON(response.SignIn{
Access: tokens.Access,
Refresh: tokens.Refresh,
})
Response download
#
return c.Download("./storage/logs/logs.log", "Log_file.log")
View #
func (m *UploadFormPage) Handle(c *gfly.Ctx) error {
return c.View("upload", gfly.ViewData{})
}
View template resources/views/upload.tpl