Uh oh! Looks like JavaScript is disabled.

Bitman requires JavaScript to fuel his thirst to end bad UX and lazy design, which is necessary to give you the best viewing experience possible. Please enable it to continue to our website.

/web - 4 min read

Leveraging DTO pattern in Go-based web apps

Shamil Siddique

Shamil Siddique

Developer

Leveraging DTO pattern in Go-based web apps

I’ll start by confessing a certain bias — I love statically-typed languages. The level of control and certainty they offer compared to dynamically-typed languages is something I always drool over.

This admiration extends to the benefits that the DTO (Data Transfer Object) pattern brings to the table. The DTO pattern is a design strategy used to transfer data between software application subsystems. It ensures structure, which tickles my love for control in my apps.

In this article, we’ll go through the implementation of DTO pattern in Go-based API. I’ll explain my DTO naming strategy as well as parsing and validation logic. For simplicity, I’ll be writing the parsing and validation part for Go Fiber framework, but most of the code can be reused for vanilla Go or other frameworks like Gorilla.

DTO Naming strategy

I usually follow a set of general rules when naming DTOs for API. These rules work for almost all CRUD cases and a couple of other cases. If they are not applicable, I try improvising on top of them.

The entity/model that is going to be affected (Article, Authentication, Socket, etc) makes the first word in the name. This can be omitted if the action in second point is self-defining (for example, authentication). The type of action (Create, Update, Signup, etc) comes second in the name. They can be chained or omitted if being re-used or if not applicable. Whether it is for Request or Response forms the third part of the name, and it’ll be omitted if they are reused for both. The DTOs are grouped into a file based on the first part of its name in singular form, which is the entity/model it affects. I’ll give a few examples which apply these rules:

  • dtos/article.go may contain CRUD DTOs like ArticleListResponseDto, ArticleCreateRequestDto, ArticleCreateUpdateResponseDto, etc.
  • dtos/authentication.go may contain more action based DTOs like SigninRequestDto, SignupRequestDto, etc.
  • dtos/socket.go may contain generic DTOs like SocketRequestDto, SocketResponseDto, etc.

Parsing and Validation

I use tags to assist in parsing to and from JSON. Tags can also be used for defining rules for each field in the DTO and validating using the validator package. So, my DTOs might looks like:

type SignupRequestDTO struct {
  Name     string `json:"name" validate:"required"`
  Username string `json:"username" validate:"required"`
  Email    string `json:"email" validate:"required,min=5"`
  Password string `json:"password" validate:"required,min=8"`
}

I personally do the parsing and validation in a single util method. This is because failure in parsing would also indicate a validation failure on the request body structure. If parsing works out well, we can validate other constraints we set for the DTO fields using the validate tags. We loop through the validation errors and generate appropriate error messages for them.

func ParseAndValidateRequest(ctx *fiber.Ctx, dto any) fiber.Map {
  if err := ctx.BodyParser(dto); err != nil {
    errors := []string{"Unable to parse request"}
    return fiber.Map{"errors": errors}
  }

  if err := validator.New().Struct(dto); err != nil {
    errors := make([]string, 0)
    for _, err := range err.(validator.ValidationErrors) {
      errors = append(errors, generateErrorString(err))
    }
    return fiber.Map{"errors": errors}
  }

  return nil
}

The generateErrorString method used here is a small method to generate error strings for a given FieldError. It checks the tag associated with the error and generates appropriate error message.

func generateErrorString(err validator.FieldError) string {
  field, tag, param := err.Field(), err.Tag(), err.Param()
  switch tag {
  case "min":
    return field + " should have minimum length of " + param
  case "max":
    return field + " should have maximum length of " + param
  default:
    return field + " is not valid"
  }
}

As we add more tags for validate, we can extend this function to handle more cases. A full list of tags can be found in its documentation or you can read through is source on Github.

Now, when I have to parse a request body to DTO, I’ll pass the pointer to the DTO. The function will parse and map to the fields in DTO passed by its pointer. If there are any errors, we get a map with errors list inside it.

requestDTO := new(dtos.SignupRequestDTO)
if errors := utils.ParseAndValidateRequest(ctx, requestDTO); errors != nil {
  return ctx.Status(fiber.StatusUnprocessableEntity).JSON(errors)
}

Now that I think about it, this errors map returned from the function can also be a DTO 😉.

Conclusion

Overall, this flow has been pretty convenient to use in my Go-based web projects. I write validation tags only in Request DTOs and only on fields that require them. But, I’ve consistently written JSON tags for all fields regardless of whether they are required or not.

Do let me know in the comments if you improvise on this.


Shamil Siddique

Shamil Siddique

Developer

He is still thinking what to write about him


Let’s build digital solutions together.
Get in touch
->
Lenny Face