API completa em Golang - Parte 3

Wiliam V. Joaquim - Dec 16 '23 - - Dev Community

O que vamos fazer?

Na parte 3 do nosso crud, vamos configurar nosso dto para realizar as validações dos dados de entrada da nossa api e vamos criar o handler do usuário.

Se ainda não viu a a parte 1 e a parte 2, recomendo você a ler antes de seguir.

Padronizando erros http

Antes de iniciar com o dto, precisamos padronizar nossos erros http, é uma boa prática criar uma padronização dos nossos erros http que retornamos para nosso cliente, por isso vamos retornar sempre nesse padrão:

{
  "message": "message error",
  "error": "bad_request",
  "code": 400,
  "fields": []
}
Enter fullscreen mode Exit fullscreen mode

Vamos retornar sempre nesse formato, o campo fields vai ser preenchido pelo go playground com os dados dos campos que estão errados, ficando nesse formato:

{
  "message": "message error",
  "error": "bad_request",
  "code": 400,
  "fields": [
    {
      "field": "email",
      "value": "email.com",
      "message": "invalid email"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Dessa forma ajuda o consumidor da nossa api a identificar os campos errados.

Vamos criar dentro da pasta handler uma pasta chamada httperr e um arquivo dentro chamado httperr, ficando assim:

  package httperr

  import "net/http"

  type RestErr struct {
    Message string   `json:"message"`
    Err     string   `json:"error,omitempty"`
    Code    int      `json:"code"`
    Fields  []Fields `json:"fields,omitempty"`
  }

  type Fields struct {
    Field   string      `json:"field"`
    Value   interface{} `json:"value,omitempty"`
    Message string      `json:"message"`
  }

  func (r *RestErr) Error() string {
    return r.Message
  }

  func NewRestErr(m, e string, c int, f []Fields) *RestErr {
    return &RestErr{
      Message: m,
      Err:     e,
      Code:    c,
      Fields:  f,
    }
  }

  func NewBadRequestError(message string) *RestErr {
    return &RestErr{
      Message: message,
      Err:     "bad_request",
      Code:    http.StatusBadRequest,
    }
  }

  func NewUnauthorizedRequestError(message string) *RestErr {
    return &RestErr{
      Message: message,
      Err:     "unauthorized",
      Code:    http.StatusUnauthorized,
    }
  }

  func NewBadRequestValidationError(m string, c []Fields) *RestErr {
    return &RestErr{
      Message: m,
      Err:     "bad_request",
      Code:    http.StatusBadRequest,
      Fields:  c,
    }
  }

  func NewInternalServerError(message string) *RestErr {
    return &RestErr{
      Message: message,
      Err:     "internal_server_error",
      Code:    http.StatusInternalServerError,
    }
  }

  func NewNotFoundError(message string) *RestErr {
    return &RestErr{
      Message: message,
      Err:     "not_found",
      Code:    http.StatusNotFound,
    }
  }

  func NewForbiddenError(message string) *RestErr {
    return &RestErr{
      Message: message,
      Err:     "forbidden",
      Code:    http.StatusForbidden,
    }
  }
Enter fullscreen mode Exit fullscreen mode

Uma função simples, que vai apenas formatar e retornar uma mensagem padrão, para cada tipo de erro http, não criei todos os erros, apenas os mais comuns, você pode ver todos os erros http aqui e implementar.

Configurando o Go Playground Validator

Vamos utilizar o Go playground validator para nos ajudar a realizar as validações, para isso vamos separar em uma pasta dentro da nossa pasta handler chamada validation o arquivo vai se chamar http_validation.go.

Nossa função vai receber uma interface vazia e vai retornar um ponteiro do nosso http_err, veja como vai ficar nossa função:

  func ValidateHttpData(d interface{}) *httperr.RestErr {
    val := validator.New(validator.WithRequiredStructEnabled())

    // extract json tag name
    val.RegisterTagNameFunc(func(fld reflect.StructField) string {
      name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
      if name == "-" {
        return ""
      }
      return name
    })

    if err := val.Struct(d); err != nil {
      var errorsCauses []httperr.Fields

      for _, e := range err.(validator.ValidationErrors) {
        cause := httperr.Fields{}
        fieldName := e.Field()

        switch e.Tag() {
        case "required":
          cause.Message = fmt.Sprintf("%s is required", fieldName)
          cause.Field = fieldName
          cause.Value = e.Value()
        case "uuid4":
          cause.Message = fmt.Sprintf("%s is not a valid uuid", fieldName)
          cause.Field = fieldName
          cause.Value = e.Value()
        case "boolean":
          cause.Message = fmt.Sprintf("%s is not a valid boolean", fieldName)
          cause.Field = fieldName
          cause.Value = e.Value()
        case "min":
          cause.Message = fmt.Sprintf("%s must be greater than %s", fieldName, e.Param())
          cause.Field = fieldName
          cause.Value = e.Value()
        case "max":
          cause.Message = fmt.Sprintf("%s must be less than %s", fieldName, e.Param())
          cause.Field = fieldName
          cause.Value = e.Value()
        case "email":
          cause.Message = fmt.Sprintf("%s is not a valid email", fieldName)
          cause.Field = fieldName
          cause.Value = e.Value()
        default:
          cause.Message = "invalid field"
          cause.Field = fieldName
          cause.Value = e.Value()
        }

        errorsCauses = append(errorsCauses, cause)
      }
      return httperr.NewBadRequestValidationError("some fields are invalid", errorsCauses)
    }
    return nil
  }
Enter fullscreen mode Exit fullscreen mode

Primeiro iniciamos o validador com validator.New(), depois extraímos o nome da tag do json, isso vai servir para informar o campo que ocasionou o erro, depois nosso val.Struct(d) realiza a validação e se houver um erro cai no if, onde verificamos cada tipo de erro, como por exemplo se for required, adicionamos esse erro no errorsCauses e assim por diante. Por fim retornamos o http_err.NewBadRequestValidationError com nosso erro personalizado.

Note que é um pouco manual para configurar, mas não precisa colocar todos os case com todas as opções, conforme for utilizando você vai incrementando essas validações. Perceba que com isso você pode colocar a mensagem de erro que desejar no cause.Message, logo você vai entender onde vamos determinar as regras dos nossos campos como max, min, required, uuidv4. Você pode ver como configurar com mais detalhes o Go playground validator na documentação oficial, essa foi a melhor forma que encontrei para utilizar em meus projetos, você pode encontrar outras implementações ou criar a sua.

Iniciando os dtos

Calma, estamos quase chegando no nosso handler, mas antes precisamos criar nossos dtos (Data Transfer Object) que tem como responsabilidade validar os dados que transitam entre as camadas da nossa aplicação, no nosso caso vamos usar para trafegar entre handler e service.

Vamos criar na pasta dto um arquivo chamado user_dto.go responsável por salvar nossas structs e será nesse arquivo onde vamos definir as regras de validações, aquelas regras que definimos no go playground na função ValidateHttpData.

  type CreateUserDto struct {
    Name     string `json:"name" validate:"required,min=3,max=30"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8,max=30,containsany=!@#$%*"`
  }
Enter fullscreen mode Exit fullscreen mode

Usamos as tags do Go para informar ao go playground as validações que desejamos para este campo, não vamos criar muitos campos para ser mais rápido, apenas isso já será suficiente para criar um usuário.

Como validamos o containsany, forçando o usuário a informar uma senha que tenha qualquer desses caracteres, precisamos atualizar nosso RegisterTagNameFunc, vamos adicionar:

  case "containsany":
    cause.Message = fmt.Sprintf("%s must contain at least one of the following characters: !@#$%%*", fieldName)
    cause.Field = fieldName
    cause.Value = e.Value()
Enter fullscreen mode Exit fullscreen mode

Finalizando o handler

Finalmente vamos partir para o nosso handler, vamos criar os seguintes endpoints:

  • Criação de usuário
  • Atualização de usuário
  • Listar usuário pelo id
  • Listar todos os usuários
  • Deletar usuário
  • Atualizar a senha do usuário

Criando o usuário

Agora dentro do nosso user_handler.go vamos iniciar a validação da request recebida:

  var req dto.CreateUserDto

  if r.Body == http.NoBody {
    slog.Error("body is empty", slog.String("package", "userhandler"))
    w.WriteHeader(http.StatusBadRequest)
    msg := httperr.NewBadRequestError("body is required")
    json.NewEncoder(w).Encode(msg)
    return
  }
Enter fullscreen mode Exit fullscreen mode

Primeiro criamos uma var do nosso dto CreateUserDto, depois verificamos se o body da requisição não é vazio, se for já retornamos um erro, perceba que já estamos usando nossos logs.

  err := json.NewDecoder(r.Body).Decode(&req)
  if err != nil {
    slog.Error("error to decode body", "err", err, slog.String("package", "handler_user"))
    w.WriteHeader(http.StatusBadRequest)
    msg := httperr.NewBadRequestError("error to decode body")
    json.NewEncoder(w).Encode(msg)
    return
  }
Enter fullscreen mode Exit fullscreen mode

Agora estamos transformando o body que é um io em uma struct Go, especificamente na struct CreateUserDto e se der um erro também retornamos retornamos o erro e finalizamos a requisição.

  httpErr := validation.ValidateHttpData(req)
  if httpErr != nil {
    slog.Error(fmt.Sprintf("error to validate data: %v", httpErr), slog.String("package", "handler_user"))
    w.WriteHeader(httpErr.Code)
    json.NewEncoder(w).Encode(httpErr)
    return
  }
Enter fullscreen mode Exit fullscreen mode

Acima chamamos o ValidateHttpData que vai colocar o go playground em ação e validar se os dados do nosso CreateUserDto estão de acordo com o que determinamos, caso não esteja retorna um erro.

Já podemos ver isso na prática, se estiver acompanhando seu projeto deve rodar sem problemas, rode com o comando go run cmd/webserver/main.go, vamos fazer uma chamada POST para http://localhost:8080/user, vou deixar no projeto o arquivo da extensão para vscode do rest client

  POST http://localhost:8080/user HTTP/1.1
  content-type: application/json

  {
    "name": "John Doe",
    "email": "john.doe@email.com",
    "password": "12345678"
  }
Enter fullscreen mode Exit fullscreen mode

Vamos ter o retorno:

  HTTP/1.1 400 Bad Request
  Date: Thu, 14 Dec 2023 23:33:21 GMT
  Content-Length: 205
  Content-Type: text/plain; charset=utf-8
  Connection: close

  {
    "message": "some fields are invalid",
    "error": "bad_request",
    "code": 400,
    "fields": [
      {
        "field": "password",
        "value": "12345678",
        "message": "password must contain at least one of the following characters: !@#$%*"
      }
    ]
  }
Enter fullscreen mode Exit fullscreen mode

Perceba que nossas validações funcionaram, e fomos informados que nosso password está inválido, precisamos colocar qualquer caracter que solicitamos como obrigatório

  POST http://localhost:8080/user HTTP/1.1
  content-type: application/json

  {
    "name": "John Doe",
    "email": "john.doe@email.com",
    "password": "12345678@"
  }
Enter fullscreen mode Exit fullscreen mode

Resposta:

  HTTP/1.1 200 OK
  Date: Thu, 14 Dec 2023 23:35:52 GMT
  Content-Length: 0
  Connection: close
Enter fullscreen mode Exit fullscreen mode

Agora sim, tudo funcionando com validações e padronização, basta chamar o service, mas precisamos ajustar nosso service do usuário, para receber os parâmetros necessários.

user_interface_service.go:

  type UserService interface {
    CreateUser(ctx context.Context, u dto.CreateUserDto) error
  }
Enter fullscreen mode Exit fullscreen mode

Agora nossa interface receber um context e o nosso dto, precisamos ajustar a implementação também:

user_service.go:

  func (s *service) CreateUser(ctx context.Context, u dto.CreateUserDto) error {
    return nil
  }
Enter fullscreen mode Exit fullscreen mode

Voltando ao nosso handler, vamos chamar nosso service:

  err = h.service.CreateUser(r.Context(), req)
  if err != nil {
    slog.Error(fmt.Sprintf("error to create user: %v", err), slog.String("package", "handler_user"))
    w.WriteHeader(http.StatusInternalServerError)
    msg := httperr.NewBadRequestError("error to create user")
    json.NewEncoder(w).Encode(msg)
    return
  }
Enter fullscreen mode Exit fullscreen mode

Pegamos o context da requisição e nosso req que é um CreateUserDto e passamos para o nosso UserService. E essa é toda a responsabilidade que nosso handler vai ter, apenas validar os dados antes de passar para o service, nosso service que vai conter a regra de negócio.

Atualizando o usuário

Para o nosso handler de atualização do usuário vamos precisar criar um novo dto:

  type UpdateUserDto struct {
    Name  string `json:"name" validate:"omitempty,min=3,max=30"`
    Email string `json:"email" validate:"omitempty,email"`
  }
Enter fullscreen mode Exit fullscreen mode

Mas desta vez não vamos usar required e vamos usar o omitempty com isso o campo se torna opcional, repare que não permitimos atualizar o password, vamos criar um endpoint separado apenas para atualizar a senha do usuário.

Vamos adicionar a nova rota, primeiro vamos criar na interface UserHandler o método UpdateUser:

  type UserHandler interface {
    CreateUser(w http.ResponseWriter, r *http.Request)
    UpdateUser(w http.ResponseWriter, r *http.Request)
  }
Enter fullscreen mode Exit fullscreen mode

Vamos adicionar a rota em user_route.go:

 router.Route("/user", func(r chi.Router) {
    r.Post("/", h.CreateUser)
    r.Patch("/{id}", h.UpdateUser)
  })
Enter fullscreen mode Exit fullscreen mode

vamos passar o {id} no param, e pegar na requisição, usaremos o patch pois nosso usuário pode ser alterado parcialmente.

Agora vamos implementar nosso UpdateUser e fazer as validações:

  var req dto.UpdateUserDto

  id := chi.URLParam(r, "id")
  if id == "" {
    slog.Error("id is empty", slog.String("package", "userhandler"))
    w.WriteHeader(http.StatusBadRequest)
    msg := httperr.NewBadRequestError("id is required")
    json.NewEncoder(w).Encode(msg)
    return
  }
Enter fullscreen mode Exit fullscreen mode

Primeiro criamos uma variável para o nosso UpdateUserDto, depois pegamos o id do usuário no param usando o URLParam do go chi, se estiver vazio retornamos um erro.

  _, err := uuid.Parse(id)
  if err != nil {
    slog.Error(fmt.Sprintf("error to parse id: %v", err), slog.String("package", "handler_user"))
    w.WriteHeader(http.StatusBadRequest)
    msg := httperr.NewBadRequestError("error to parse id")
    json.NewEncoder(w).Encode(msg)
    return
  }
Enter fullscreen mode Exit fullscreen mode

Depois validamos se o id que extraímos do param é um uuid válido, para isso usamos o uuid.Parse(id) do pacote do google uuid.

  if r.Body == http.NoBody {
    slog.Error("body is empty", slog.String("package", "userhandler"))
    w.WriteHeader(http.StatusBadRequest)
    msg := httperr.NewBadRequestError("body is required")
    json.NewEncoder(w).Encode(msg)
    return
  }
  err = json.NewDecoder(r.Body).Decode(&req)
  if err != nil {
    slog.Error("error to decode body", "err", err, slog.String("package", "handler_user"))
    w.WriteHeader(http.StatusBadRequest)
    msg := httperr.NewBadRequestError("error to decode body")
    json.NewEncoder(w).Encode(msg)
    return
  }
  httpErr := validation.ValidateHttpData(req)
  if httpErr != nil {
    slog.Error(fmt.Sprintf("error to validate data: %v", httpErr), slog.String("package", "handler_user"))
    w.WriteHeader(httpErr.Code)
    json.NewEncoder(w).Encode(httpErr)
    return
  }
Enter fullscreen mode Exit fullscreen mode

Agora fazemos o mesmo que na criação de usuário, validamos se o body não está vazio, transformamos o body na struct UpdateUserDto, e validamos o UpdateUserDto usando nosso validator ValidateHttpData.

Por fim, vamos adicionar o método de UpdateUser no nosso service que vai receber um context, UpdateUserDto e o id do usuário para finalizar o handler de atualizar usuário:

user_interface_service.go:

  type UserService interface {
    CreateUser(ctx context.Context, u dto.CreateUserDto) error
    UpdateUser(ctx context.Context, u dto.UpdateUserDto, id string) error
  }
Enter fullscreen mode Exit fullscreen mode

Implementando o UpdateUser service:

  func (s *service) UpdateUser(ctx context.Context, u dto.UpdateUserDto, id string) error {
    return nil
  }
Enter fullscreen mode Exit fullscreen mode

Agora vamos finalizar o handler de UpdateUser:

  err = h.service.UpdateUser(r.Context(), req, id)
  if err != nil {
    slog.Error(fmt.Sprintf("error to update user: %v", err), slog.String("package", "handler_user"))
    w.WriteHeader(http.StatusInternalServerError)
    msg := httperr.NewBadRequestError("error to update user")
    json.NewEncoder(w).Encode(msg)
    return
  }
Enter fullscreen mode Exit fullscreen mode

Vamos testar:

  ## Update user
  PATCH http://localhost:8080/user/9d47a06e-9677-460c-84e3-1bb7ac20b931 HTTP/1.1
  content-type: application/json

  {
    "name": "John Doe",
    "email": "john.doe@email.com"
  }
Enter fullscreen mode Exit fullscreen mode

Retorno:

  HTTP/1.1 200 OK
  Date: Sat, 16 Dec 2023 19:06:12 GMT
  Content-Length: 0
  Connection: close
Enter fullscreen mode Exit fullscreen mode

Detalhes do usuário

Vamos buscar um usuário pelo id, para isso não vamos precisar de um dto, vamos apenas buscar pelo id que vamos pegar no param, já vamos criar as rotas e o método na interface do nosso service e handler. Mas antes vamos criar uma pasta chamada response e um arquivo chamado user_response.go, vamos armazenar nesse arquivo as structs do retorno para o nosso cliente.

  type UserResponse struct {
    ID        string    `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
  }
Enter fullscreen mode Exit fullscreen mode

Dessa forma mapeamos os dados a serem retornados, em nosso service ficaria assim:

  type UserService interface {
    CreateUser(ctx context.Context, u dto.CreateUserDto) error
    UpdateUser(ctx context.Context, u dto.UpdateUserDto, id string) error
    GetUserByID(ctx context.Context, id string) (*response.UserResponse, error)
  }
Enter fullscreen mode Exit fullscreen mode

Retornando um ponteiro do *response.UserResponse, o handler GetUserByID vai ficar assim:

  func (h *handler) GetUserByID(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    if id == "" {
      slog.Error("id is empty", slog.String("package", "userhandler"))
      w.WriteHeader(http.StatusBadRequest)
      msg := httperr.NewBadRequestError("id is required")
      json.NewEncoder(w).Encode(msg)
      return
    }
    _, err := uuid.Parse(id)
    if err != nil {
      slog.Error(fmt.Sprintf("error to parse id: %v", err), slog.String("package", "handler_user"))
      w.WriteHeader(http.StatusBadRequest)
      msg := httperr.NewBadRequestError("error to parse id")
      json.NewEncoder(w).Encode(msg)
      return
    }
    res, err := h.service.GetUserByID(r.Context(), id)
    if err != nil {
      slog.Error(fmt.Sprintf("error to get user: %v", err), slog.String("package", "handler_user"))
      w.WriteHeader(http.StatusInternalServerError)
      msg := httperr.NewBadRequestError("error to get user")
      json.NewEncoder(w).Encode(msg)
      return
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(res)
  }
Enter fullscreen mode Exit fullscreen mode

Mesma lógica dos demais, mas agora nosso h.service.GetUserByID retorna o response.UserResponse retornamos o dado em json para o client, usando o json.NewEncoder(w).Encode(res).

Vamos testar, mas como nosso service não está finalizado, vamos retornar dados fakes:

user_service.go

  func (s *service) GetUserByID(ctx context.Context, id string) (*response.UserResponse, error) {
    userFake := response.UserResponse{
      ID:        "123",
      Name:      "John Doe",
      Email:     "jonh.doe@email.com",
      CreatedAt: time.Now(),
      UpdatedAt: time.Now(),
    }
    return &userFake, nil
  }
Enter fullscreen mode Exit fullscreen mode

Fazendo uma chamada:

  ## GetUserByID
  GET http://localhost:8080/user/9d47a06e-9677-460c-84e3-1bb7ac20b931 HTTP/1.1
  content-type: application/json
Enter fullscreen mode Exit fullscreen mode

Retorno:

  HTTP/1.1 200 OK
  Content-Type: application/json
  Date: Sat, 16 Dec 2023 19:42:11 GMT
  Content-Length: 156
  Connection: close

  {
    "id": "123",
    "name": "John Doe",
    "email": "jonh.doe@email.com",
    "created_at": "2023-12-16T16:42:11.816803-03:00",
    "updated_at": "2023-12-16T16:42:11.816803-03:00"
  }
Enter fullscreen mode Exit fullscreen mode

Deletando o usuário

O endpoint de deletar vai ser muito semelhante ao endpoint de listar usuário, a diferença vai ser apenas o método do service a ser chamado e não vai ter retorno:

user_interface_handler.go:

  type UserHandler interface {
    CreateUser(w http.ResponseWriter, r *http.Request)
    UpdateUser(w http.ResponseWriter, r *http.Request)
    GetUserByID(w http.ResponseWriter, r *http.Request)
    DeleteUser(w http.ResponseWriter, r *http.Request)
  }
Enter fullscreen mode Exit fullscreen mode

user_route.go:

  router.Route("/user", func(r chi.Router) {
    r.Post("/", h.CreateUser)
    r.Patch("/{id}", h.UpdateUser)
    r.Get("/{id}", h.GetUserByID)
    r.Delete("/{id}", h.DeleteUser)
  })
Enter fullscreen mode Exit fullscreen mode

user_handler.go:

  func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    if id == "" {
      slog.Error("id is empty", slog.String("package", "userhandler"))
      w.WriteHeader(http.StatusBadRequest)
      msg := httperr.NewBadRequestError("id is required")
      json.NewEncoder(w).Encode(msg)
      return
    }
    _, err := uuid.Parse(id)
    if err != nil {
      slog.Error(fmt.Sprintf("error to parse id: %v", err), slog.String("package", "handler_user"))
      w.WriteHeader(http.StatusBadRequest)
      msg := httperr.NewBadRequestError("error to parse id")
      json.NewEncoder(w).Encode(msg)
      return
    }
    err = h.service.DeleteUser(r.Context(), id)
    if err != nil {
      slog.Error(fmt.Sprintf("error to delete user: %v", err), slog.String("package", "handler_user"))
      w.WriteHeader(http.StatusInternalServerError)
      msg := httperr.NewBadRequestError("error to delete user")
      json.NewEncoder(w).Encode(msg)
      return
    }
    w.WriteHeader(http.StatusNoContent)
  }
Enter fullscreen mode Exit fullscreen mode

Chamada http:

  ## DeleteUser
  DELETE http://localhost:8080/user/9d47a06e-9677-460c-84e3-1bb7ac20b931 HTTP/1.1
  content-type: application/json
Enter fullscreen mode Exit fullscreen mode

Listar todos os usuários

Esse endpoint vai ter o handler mais simples, pois não vamos precisar validar dados de entrada, apenas vamos listar, podemos futuramente colocar paginação, filtros, mas por enquanto vamos apenas listar de forma simples, para isso vamos precisar criar um novo response no nosso arquivo user_response.go:

  type ManyUsersResponse struct {
    Users []UserResponse `json:"users"`
  }
Enter fullscreen mode Exit fullscreen mode

Vai apenas retornar um slice da struct que já criamos anteriormente.

user_route.go:

  router.Route("/user", func(r chi.Router) {
    r.Post("/", h.CreateUser)
    r.Patch("/{id}", h.UpdateUser)
    r.Get("/{id}", h.GetUserByID)
    r.Delete("/{id}", h.DeleteUser)
    r.Get("/", h.FindManyUsers)
  })
Enter fullscreen mode Exit fullscreen mode

user_interface_service.go:

  type UserService interface {
    CreateUser(ctx context.Context, u dto.CreateUserDto) error
    UpdateUser(ctx context.Context, u dto.UpdateUserDto, id string) error
    GetUserByID(ctx context.Context, id string) (*response.UserResponse, error)
    DeleteUser(ctx context.Context, id string) error
    FindManyUsers(ctx context.Context) (response.ManyUsersResponse, error)
  }
Enter fullscreen mode Exit fullscreen mode

Implementando o service, vamos fazer um loop retornando 5 usuários fakes:

  func (s *service) FindManyUsers(ctx context.Context) (response.ManyUsersResponse, error) {
    // create fake users
    usersFake := response.ManyUsersResponse{}
    for i := 0; i < 5; i++ {
      userFake := response.UserResponse{
        ID:        "123",
        Name:      "John Doe",
        Email:     fmt.Sprintf("jonh.doe-%v@email.com", i),
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
      }
      usersFake.Users = append(usersFake.Users, userFake)
    }
    return usersFake, nil
  }
Enter fullscreen mode Exit fullscreen mode

Nosso handler ficará assim:

  func (h *handler) FindManyUsers(w http.ResponseWriter, r *http.Request) {
    res, err := h.service.FindManyUsers(r.Context())
    if err != nil {
      slog.Error(fmt.Sprintf("error to find many users: %v", err), slog.String("package", "handler_user"))
      w.WriteHeader(http.StatusInternalServerError)
      msg := httperr.NewBadRequestError("error to find many users")
      json.NewEncoder(w).Encode(msg)
      return
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(res)
  }
Enter fullscreen mode Exit fullscreen mode

Simplesmente vamos chamar o service e retornamos os dados.

Alterando a senha

Nosso último endpoint para o usuário vai permitir que ele altere a senha, é uma boa prática separar em outro enpoint, dessa forma podemos aplicar políticas de validação mais rigorosas, para nosso exemplo vamos apenas solicitar a senha antiga e a nova senha.

Vamos criar um dto para isso:

  type UpdateUserPasswordDto struct {
    Password    string `json:"password" validate:"required,min=8,max=30,containsany=!@#$%*"`
    OldPassword string `json:"old_password" validate:"required,min=8,max=30,containsany=!@#$%*"`
  }
Enter fullscreen mode Exit fullscreen mode

user_route.go:

  router.Route("/user", func(r chi.Router) {
    r.Post("/", h.CreateUser)
    r.Patch("/{id}", h.UpdateUser)
    r.Get("/{id}", h.GetUserByID)
    r.Delete("/{id}", h.DeleteUser)
    r.Get("/", h.FindManyUsers)
    r.Patch("/password/{id}", h.UpdateUserPassword)
  })
Enter fullscreen mode Exit fullscreen mode

user_interface_service.go:

  type UserService interface {
    CreateUser(ctx context.Context, u dto.CreateUserDto) error
    UpdateUser(ctx context.Context, u dto.UpdateUserDto, id string) error
    GetUserByID(ctx context.Context, id string) (*response.UserResponse, error)
    DeleteUser(ctx context.Context, id string) error
    FindManyUsers(ctx context.Context) (response.ManyUsersResponse, error)
    UpdateUserPassword(ctx context.Context, u *dto.UpdateUserPasswordDto, id string) error
  }
Enter fullscreen mode Exit fullscreen mode

Implementando o service, vamos apenas usar o fmt para ver o dado recebido, depois vamos adicionar a regra aqui.

  func (s *service) UpdateUserPassword(ctx context.Context, u *dto.UpdateUserPasswordDto, id string) error {
    fmt.Println("new password: ", u.Password)
    fmt.Println("old password: ", u.OldPassword)
    return nil
  }
Enter fullscreen mode Exit fullscreen mode

Nosso handler ficará assim:

  func (h *handler) UpdateUserPassword(w http.ResponseWriter, r *http.Request) {
    var req dto.UpdateUserPasswordDto

    id := chi.URLParam(r, "id")
    if id == "" {
      slog.Error("id is empty", slog.String("package", "userhandler"))
      w.WriteHeader(http.StatusBadRequest)
      msg := httperr.NewBadRequestError("id is required")
      json.NewEncoder(w).Encode(msg)
      return
    }
    _, err := uuid.Parse(id)
    if err != nil {
      slog.Error(fmt.Sprintf("error to parse id: %v", err), slog.String("package", "handler_user"))
      w.WriteHeader(http.StatusBadRequest)
      msg := httperr.NewBadRequestError("error to parse id")
      json.NewEncoder(w).Encode(msg)
      return
    }
    if r.Body == http.NoBody {
      slog.Error("body is empty", slog.String("package", "userhandler"))
      w.WriteHeader(http.StatusBadRequest)
      msg := httperr.NewBadRequestError("body is required")
      json.NewEncoder(w).Encode(msg)
      return
    }
    err = json.NewDecoder(r.Body).Decode(&req)
    if err != nil {
      slog.Error("error to decode body", "err", err, slog.String("package", "handler_user"))
      w.WriteHeader(http.StatusBadRequest)
      msg := httperr.NewBadRequestError("error to decode body")
      json.NewEncoder(w).Encode(msg)
      return
    }
    httpErr := validation.ValidateHttpData(req)
    if httpErr != nil {
      slog.Error(fmt.Sprintf("error to validate data: %v", httpErr), slog.String("package", "handler_user"))
      w.WriteHeader(httpErr.Code)
      json.NewEncoder(w).Encode(httpErr)
      return
    }
    err = h.service.UpdateUserPassword(r.Context(), &req, id)
    if err != nil {
      slog.Error(fmt.Sprintf("error to update user password: %v", err), slog.String("package", "handler_user"))
      w.WriteHeader(http.StatusInternalServerError)
      msg := httperr.NewBadRequestError("error to update user password")
      json.NewEncoder(w).Encode(msg)
      return
    }
  }
Enter fullscreen mode Exit fullscreen mode

Fazemos as validações iniciais, transformamos na nossa struct UpdateUserPasswordDto, enviamos para validar usando nosso ValidateHttpData e por fim chamamos o service, seguindo sempre a mesma lógicas dos demais endpoints.

Com isso finalizamos os handlers do nosso user, lembrando que não estamos muito preocupados com os dados que o usuário pode ter, se desejar pode adicionar mais dados, como idade do usuário, documento e mais para frente vamos voltar e editar, para cadastrar o endereço do usuário ao cadastrá-lo.

Documentando

Com os endpoints praticamente prontos, podemos fazer nossa documentação usando o swaggo para criar nossa documentação no padrão OpenAPI. Já escrevi um post sobre Como deixar o Swagger com tema dark mode usando Swaggo e Golang, mas para instalar o swaggo é bem simples, veja como neste link, já vamos deixar o swagger em dark mode é claro.

Vamos criar um endpoint que vai servir para acessar nossa documentação, vamos criar na pasta routes um arquivo chamado docs_route.go

  var (
    docsURL = "http://localhost:8080/docs/doc.json"
  )

//  @title      Swagger Dark Mode
//  @version    1.0
//  @securityDefinitions.apikey ApiKeyAuth
//  @in                         header
//  @name                       Authorization
  func InitDocsRoutes(r chi.Router) {
    r.Get("/docs/*", httpSwagger.Handler(httpSwagger.URL(docsURL),
      httpSwagger.AfterScript(custom.CustomJS),
      httpSwagger.DocExpansion("none"),
      httpSwagger.UIConfig(map[string]string{
        "defaultModelsExpandDepth": `"-1"`,
      }),
    ))
  }
Enter fullscreen mode Exit fullscreen mode

não se esqueça de fazer o download do swaggo com go mod tidy

Todas a explicação dessas configs estão no post Como deixar o Swagger com tema dark mode usando Swaggo e Golang.

Vamos precisar iniciar o InitDocsRoutes no nosso main.go:

 // init routes
  router := chi.NewRouter()
  routes.InitUserRoutes(router, newUserHandler)
  routes.InitDocsRoutes(router)
Enter fullscreen mode Exit fullscreen mode

Agora podemos documentar, vamos iniciar pela criação de usuário, basta colocar as anotações acima da função CreateUser:

  // Create user
  //    @Summary        Create new user
  //    @Description    Endpoint for create user
  //    @Tags           user
  //    @Accept         json
  //    @Produce        json
  //    @Param          body    body        dto.CreateUserDto   true    "Create user dto"   true
  //    @Success        200
  //    @Failure        400 {object}    httperr.RestErr
  //    @Failure        500 {object}    httperr.RestErr
  //    @Router         /user [get]
  func (h *handler) CreateUser(w http.ResponseWriter, r *http.Request){}
Enter fullscreen mode Exit fullscreen mode

Vamos fazer isso para todos os funções:

  // Update user
  //    @Summary        Update user
  //    @Description    Endpoint for update user
  //    @Tags           user
  //    @Security       ApiKeyAuth
  //    @Accept         json
  //    @Produce        json
  //    @Param          body    body    dto.UpdateUserDto   false   "Update user dto"   true
  //    @Success        200
  //    @Failure        400 {object}    httperr.RestErr
  //    @Failure        404 {object}    httperr.RestErr
  //    @Failure        500 {object}    httperr.RestErr
  //    @Router         /user [patch]
  func (h *handler) UpdateUser(w http.ResponseWriter, r *http.Request){}
Enter fullscreen mode Exit fullscreen mode

Algumas rotas são autenticadas, por isso vamos usar o @Security ApiKeyAuth, vamos proteger essas rotas em breve.

  // User details
  //    @Summary        User details
  //    @Description    Get user by id
  //    @Tags           user
  //    @Security       ApiKeyAuth
  //    @Accept         json
  //    @Produce        json
  //    @Param          id  path    string  true    "user id"
  //    @Success        200 {object} response.UserResponse
  //    @Failure        400 {object}    httperr.RestErr
  //    @Failure        404 {object}    httperr.RestErr
  //    @Failure        500 {object}    httperr.RestErr
  //    @Router         /user/{id} [get]
  func (h *handler) GetUserByID(w http.ResponseWriter, r *http.Request){}
Enter fullscreen mode Exit fullscreen mode
  // Delete user
  //    @Summary        Delete user
  //    @Description    delete user by id
  //    @Tags           user
  //    @Security       ApiKeyAuth
  //    @Accept         json
  //    @Produce        json
  //    @Param          id  path    string  true    "user id"
  //    @Success        200
  //    @Failure        400 {object}    httperr.RestErr
  //    @Failure        404 {object}    httperr.RestErr
  //    @Failure        500 {object}    httperr.RestErr
  //    @Router         /user/{id} [delete]
  func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request){}
Enter fullscreen mode Exit fullscreen mode
  // Get many user
  //    @Summary        Get many users
  //    @Description    Get many users
  //    @Tags           user
  //    @Security       ApiKeyAuth
  //    @Accept         json
  //    @Produce        json
  //    @Success        200 {object}    response.ManyUsersResponse
  //    @Failure        400 {object}    httperr.RestErr
  //    @Failure        404 {object}    httperr.RestErr
  //    @Failure        500 {object}    httperr.RestErr
  //    @Router         /user [get]
  func (h *handler) FindManyUsers(w http.ResponseWriter, r *http.Request){}
Enter fullscreen mode Exit fullscreen mode
  // Update user password
  //    @Summary        Update user password
  //    @Description    Endpoint for Update user password
  //    @Tags           user
  //    @Security       ApiKeyAuth
  //    @Accept         json
  //    @Produce        json
  //    @Param          id      path    string                      true    "user id"
  //    @Param          body    body    dto.UpdateUserPasswordDto   true    "Update user password dto"  true
  //    @Success        200
  //    @Failure        400 {object}    httperr.RestErr
  //    @Failure        500 {object}    httperr.RestErr
  //    @Router         /user/password/{id} [get]
  func (h *handler) UpdateUserPassword(w http.ResponseWriter, r *http.Request){}
Enter fullscreen mode Exit fullscreen mode

Agora podemos formatar a anotações com swag fmt e depois criar a pasta docs com o comando:

  swag init -g internal/handler/routes/docs_route.go
Enter fullscreen mode Exit fullscreen mode

Agora ao acessar a rota http://localhost:8080/docs/index.html vamos ter nossa documentação rodando:

Project structure

Com isso finalizamos nossa documentação.

Considerações finais

Nesse post fizemos muita coisa na nossa api, mas conseguimos deixar o handler do usuário praticamente pronto para receber nossas requisições, também deixamos a função responsável por validar nossos dados pronta, agora basta ir incrementando as validações. Também conseguimos configurar e deixar nossa api documentada com o uso do swaggo, agora ficou mais fácil documentar.

Próximos passos

Na parte 4 vamos partir para o service, deixar os métodos CreateUser, UpdateUser, GetUserByID, DeleteUser, FindManyUsers, UpdateUserPassword com a regra de negócio pronta.

Veja a parte 1 e parte 2

Link do repositório

repositório do projeto

link do projeto no meu blog

Gopher credits

. . . . . . . . . . . . . . . . . . . . . .