API completa em Golang - Parte 3
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 parte 1 e a parte 2 leia esse post primeiro.
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": []
}
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"
}
]
}
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,
}
}
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
}
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=!@#$%*"`
}
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()
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
}
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
}
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
}
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"
}
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: !@#$%*"
}
]
}
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@"
}
Resposta:
HTTP/1.1 200 OK
Date: Thu, 14 Dec 2023 23:35:52 GMT
Content-Length: 0
Connection: close
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
}
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
}
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
}
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"`
}
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)
}
Vamos adicionar a rota em user_route.go
:
router.Route("/user", func(r chi.Router) {
r.Post("/", h.CreateUser)
r.Patch("/{id}", h.UpdateUser)
})
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
}
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
}
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
}
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
}
Implementando o UpdateUser
service:
func (s *service) UpdateUser(ctx context.Context, u dto.UpdateUserDto, id string) error {
return nil
}
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
}
Vamos testar:
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"
}
Retorno:
HTTP/1.1 200 OK
Date: Sat, 16 Dec 2023 19:06:12 GMT
Content-Length: 0
Connection: close
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"`
}
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)
}
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)
}
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
}
Fazendo uma chamada:
GET http://localhost:8080/user/9d47a06e-9677-460c-84e3-1bb7ac20b931 HTTP/1.1
content-type: application/json
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"
}
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)
}
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)
})
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)
}
Chamada http:
DELETE http://localhost:8080/user/9d47a06e-9677-460c-84e3-1bb7ac20b931 HTTP/1.1
content-type: application/json
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"`
}
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)
})
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)
}
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
}
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)
}
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=!@#$%*"`
}
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)
})
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
}
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
}
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
}
}
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"`,
}),
))
}
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)
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){}
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){}
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){}
// 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){}
// 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){}
// 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){}
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
Agora ao acessar a rota http://localhost:8080/docs/index.html
vamos ter nossa documentação rodando:
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.
Link do repositório
repositório do projeto