Controllers

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