DEV Community

Cover image for πŸ’­ How to make clear & pretty error messages from the Go backend to your frontend?
Vic ShΓ³stak
Vic ShΓ³stak

Posted on

πŸ’­ How to make clear & pretty error messages from the Go backend to your frontend?

Introduction

Hey-hey, awesome DEV people! πŸ˜‰

How about a little time in the company of an article that teaches you how to better communicate with the frontend developers on your team? Intrigued, but don't know what it's about? Don't worry, I'll explain it all now!

πŸ‘€ I often notice an interesting trend in my work: the backend developer sends error statuses and explanations to the frontend, which are not always clear how to handle and output to the user in the application. As a result, the frontend developer spends precious time understanding what's going on and implementing borderline cases in the code.

πŸ’‘ But, what if I told you that errors from the backend can be not just readable to the developer, but understandable even to the user? Yes, that's accurately what this article will talk about!

We will take a look at package go-playground/validator v10, which is almost the number one choice for such problems in Go.

πŸ“ Table of contents

Model to validate

Imagine we need to implement backend validation of incoming data from POST request to REST API endpoint of new project creation. Can you imagine? Okay, now let's describe this structure in Go code.

Besides the standard attributes db:"..." and json:"...", we need to add a new attribute validate:"..." with the required validation tag from the go-playground/validator package to each structure field that we need to validate.

It only sounds very complicated, in fact, everything is much simpler. Look:

// ./app/models/project_model.go

// Project struct to describe project.
type Project struct {
    Title string `db:"title" json:"title" validate:"required,lte=25"` 
    // --> verify that the field exists and is less than or equal to 25 characters

    Description string `db:"description" json:"description" validate:"required"`
    // --> verify that the field exists

    WebsiteURL string `db:"website_url" json:"website_url" validate:"uri"`
    // --> verify that the field is correct URL string

    Tags []string `db:"tags" json:"tags" validate:"len=3"`
    // --> verify that the field contains exactly three elements
}
Enter fullscreen mode Exit fullscreen mode

☝️ Note: These are not all the parameters by which you can configure field validation for your structures! You can find the full list here.

An interesting thing is that if we specify a validator to check, for example, if the URL validated, then we don't need to specify the required validation tag anymore. This happens because an empty string is not a valid URL.

In other words, almost any validation tag will also include a mandatory non-empty value (empty string, zero, nil, …) for the field.

↑ Table of contents

Vanilla representation of the error from package

Input JSON body (here and below, we will work with these very input parameters for the JSON request body):

{
  "title": "",
  "description": "",
  "website_url": "not-valid-uri",
  "tags": [
    "one", "two"
  ]
}
Enter fullscreen mode Exit fullscreen mode

I will display the resulting error response as plain text so that you can appreciate why this presentation option for the frontend would not be good:

Key: 'Project.Title' Error:Field validation for 'Title' failed on the 'required' tag

Key: 'Project.Description' Error:Field validation for 'Description' failed on the 'required' tag

Key: 'Project.WebsiteURL' Error:Field validation for 'WebsiteURL' failed on the 'uri' tag

Key: 'Project.Tags' Error:Field validation for 'Tags' failed on the 'len' tag
Enter fullscreen mode Exit fullscreen mode

And there are several important points that we want to improve right away.

First, the frontend knows nothing about the structures and models in our application. If the backend returns an error in this form (without specifying at least the field that failed validation), the frontend will not be able to make a visual output of the error for a particular field.

Second, it's better to specify the exact names of the fields which the frontend works with β€” not WebsiteURL but website_url, like in JSON.

Third, the error description itself will not tell the user (or even the frontend developer) anything useful, except that something went wrong somewhere.

Well, let's try to improve it!

☝️ Note: I will show you the way I do it on my projects. By the way, I'd be happy to get feedback and examples of how you customize error output for frontend in your projects.

↑ Table of contents

Recreate validator

Great, we get rid of the fields with names, like in the structure. We just override their output, so that the validator looks at the json:"..." parameter in the structure, not at its actual name.

To complete this, we use the RegisterTagNameFunc method built into the package with a little magic. I will put this in a different helper package (./pkg/utilities/validator.go) so that there is more readable application code:

// ./pkg/utilities/validator.go

// NewValidator func for create a new validator for struct fields.
func NewValidator() *validator.Validate {
    // Create a new validator.
    validate := validator.New()

    // Rename struct fields to JSON.
    validate.RegisterTagNameFunc(func(fl reflect.StructField) string {
        name := strings.SplitN(fl.Tag.Get("json"), ",", 2)
        if name[1] == "-" {
            return ""
        }
        return name[0]
    })

    return validate
}

// ...
Enter fullscreen mode Exit fullscreen mode

If you want to avoid renaming any of the fields, add ,- (comma + dash) to the end of its JSON name, like this:

WebsiteURL string `db:"website_url" json:"website_url,-" validate:"uri"`
Enter fullscreen mode Exit fullscreen mode

Yes, you got me right, this method opens up great possibilities to customize the error output itself. You can rely not on json:"..." attribute in the field, but on your one, for example, field_name:"..." or any other one you wish.

☝️ Note: To understand how it works, please follow this issue.

↑ Table of contents

Function to check the validation error

Let's move on. It's time to make a nicer output of validation errors, so that the frontend developer on your team will thank you.

I always use this practice when implementing a REST API in JSON format for internal use (e.g., for single-page applications aka SPA):

  1. We return JSON in strictly consistent notation with the frontend, but relative to the interaction objects;
  2. The status code of the response from the backend is always HTTP 200 OK, unless it concerns server errors (status code 5XX);
  3. The server response always contains the status field (type int) indicating the actual status code;
  4. If an error occurred (status code not 2Π₯Π₯), the server response always contains a field msg (type string) with a short indication of the cause of the error;

Furthermore, in the example below, I took code from my project written using the Fiber web framework. So, some elements from its libraries are present there. Don't be scared, the main thing here is to understand the principle of validation itself.

☝️ Note: If you want to learn more about Fiber, I have a series of articles to help you do that. I'd be glad if you'd study it later, too.

Okay, my function to check for validation errors would look like this:

// ./pkg/utilities/validator.go

// ...

// CheckForValidationError func for checking validation errors in struct fields.
func CheckForValidationError(ctx *fiber.Ctx, errFunc error, statusCode int, object string) error {
    if errFunc != nil {
        return ctx.JSON(&fiber.Map{
            "status": statusCode,
            "msg":    fmt.Sprintf("validation errors for the %s fields", object),
            "fields": ValidatorErrors(errFunc),
        })
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

The principle of this function is elementary:

  • Accept the Fiber context to have all the possibilities to work with the context that came;
  • Accept the object with the validation error defined above;
  • Accept the status code, which should return if the error occurs;
  • Accept the name of the object (or model) we're currently checking, so we can output a more readable error message;
  • Return the generated JSON with all the necessary errors and explanations or nil;

I can now easily use the CheckForValidationError function in the controller:

// ./app/controllers/project_controller.go

import (
    // ...
    "github.com/my-user/my-repo/pkg/utilities" 
    // --> add local package `utilities`
)

// CreateNewProject func for create a new project.
func CreateNewProject(c *fiber.Ctx) error {

    // ...

    // Create a new validator, using helper function.
    validate := utilities.NewValidator()

    // Validate all incomming fields for rules in Project struct.
    if err := validate.Struct(project); err != nil {
        // Returning error in JSON format with status code 400 (Bad Request).
        return utilities.CheckForValidationError(
            c, err, fiber.StatusBadRequest, "project",
        )
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

↑ Table of contents

Custom validation tag

Every so often, it happens that the built-in validation tags (or rather, the rules by which a particular field should be validated) are not always sufficient. To solve this problem, the authors of go-playground/validator package provided a special method.

Let's consider its use on a simple example πŸ‘‡

So, we have a field with type uuid.UUID which we create with the package google/uuid, which we want to check with the built-in validator uuid.Parse() of that package. All we need to do is add a new RegisterValidation method to the NewValidator function (described above) with simple logic code:

// ./pkg/utilities/validator.go

// NewValidator func for create a new validator for struct fields.
func NewValidator() *validator.Validate {
    // Create a new validator.
    validate := validator.New()

    // ...

    // Custom validation for uuid.UUID fields.
    _ = validate.RegisterValidation("uuid", func(fl validator.FieldLevel) bool {
        field := fl.Field().String() // convert to string
        if _, err := uuid.Parse(field); err != nil {
            return true // field has error
        }
        return false // field has no error
    })

    // ...

    return validate
}
Enter fullscreen mode Exit fullscreen mode

That's it! If the field passed validation, it will return false logical value, and if there are any errors it will return true.

☝️ Note: The method RegisterValidation should be read and understood like this: β€œplease check if there is an error in the value of the field with the validation tag uuid?”.

Now we can validate fields of this type like this:

// ./app/models/something_model.go

// MyStruct struct to describe something.
type MyStruct struct {
    ID uuid.UUID `db:"id" json:"id" validate:"uuid"` 
    // --> verify that the field is a valid UUID
}
Enter fullscreen mode Exit fullscreen mode

↑ Table of contents

Override error message

Override error message

And now for the best part. Overriding the validation error message itself.

☝️ Note: Follow comments in the code to better understand.

This helper function will map all validation errors to each field and then simply pass that map to the CheckForValidationError function (which we described in the previous section):

// ./pkg/utilities/validator.go

// ...

// ValidatorErrors func for show validation errors for each invalid fields.
func ValidatorErrors(err error) map[string]string {
    // Define variable for error fields.
    errFields := map[string]string{}

    // Make error message for each invalid field.
    for _, err := range err.(validator.ValidationErrors) {
        // Get name of the field's struct.
        structName := strings.Split(err.Namespace(), ".")[0]
        // --> first (0) element is the founded name

        // Append error message to the map, where key is a field name,
        // and value is an error description.
        errFields[err.Field()] = fmt.Sprintf(
            "failed '%s' tag check (value '%s' is not valid for %s struct)",
            err.Tag(), err.Value(), structName,
        )
    }

    return errFields
}
Enter fullscreen mode Exit fullscreen mode

As you may have noticed, to override the field error message, we operate on special variables (err.Namespace(), err.Field(), err.Tag() and err.Value()) which the authors of the go-playground/validator package offer us.

☝️ Note: You can find a complete list of all available ones here.

Now, we will get this message when we make an invalid request:

{
  "status": 400,
  "msg": "validation errors for the project fields",
  "fields": {
    "category": "failed 'required' tag check (value '' is not valid for Project struct)",
    "description": "failed 'required' tag check (value '' is not valid for Project struct)",
    "tags": "failed 'len' tag check (value '[one two]' is not valid for Project struct)"
    "title": "failed 'required' tag check (value '' is not valid for Project struct)"
    "website_url": "failed 'uri' tag check (value 'not-valid-uri' is not valid for Project struct)"
  }
}
Enter fullscreen mode Exit fullscreen mode

☝️ Note: After validation, all not valid fields are in alphabetical order, not in the order that was defined by the Go structure.

Hooray! πŸŽ‰ We got what we wanted and no one got hurt. On the contrary, everyone is happy, both on the frontend and the backend.

↑ Table of contents

Photos and videos by

P.S.

If you want more articles like this on this blog, then post a comment below and subscribe to me. Thanks! 😘

Discussion (0)