All the codes that you will see in this post you can check here
In this post, we will create an Http POST method to include our books in the application and validate the fields.
Let's go to our handler and create the new endpoint to include our books
//cmd/http/handler/book.go
func (b *BookHandler) Route(r chi.Router) {
r.Get("/hello", b.hello)
r.Get("/", b.getAll)
r.Post("/", b.create)
}
Before we show the create method we need to create a struct to represent the data that will represent the request body to our handler. It's called data transfer object pattern or DTO. The DTO it's a struct that will be used to represent a data for every input data in our handler.
So we will create a new 'presenter' folder in the cmd/http folder. The name could be DTO folder or anything else that represent the DTO layer. I like to use presenter name.
//cmd/http/presenter/book.go
type BookPersist struct {
Title string `json:"title"`
Author string `json:"author"`
NumberPages int `json:"numberPages"`
}
func (b *BookPersist) ToDomain() book.Book {
return book.Book{
Title: b.Title,
Author: b.Author,
NumberPages: b.NumberPages,
}
}
This struct represent the JSON that will be received in the post body. You could be thinking why I not using the book domain struct and it's a simple reason. If your endpoint need to add some fields that not represent information about your domain then you need to add these fields in your domain model and this is not a good decision to keep with your application more cohesive.
And the ToDomain() method it's a way to propagate only the domain data through the application.
Let's go to our create method.
//cmd/http/handler/book.go
var ErrDecodeJson = errors.New("error decoding json")
func (h *BookHandler) create(w http.ResponseWriter, r *http.Request) {
dto := presenter.BookPersist{}
e := json.NewDecoder(r.Body).Decode(&dto)
if e != nil {
helper.HandleError(w, ErrDecodeJson)
return
}
entity := dto.ToDomain()
if _, err := h.bookService.Save(entity); err != nil {
helper.HandleError(w, err)
} else {
w.WriteHeader(http.StatusCreated)
}
}
First we just instantiate the dto and decode the json body to our struct. After that we convert our DTO to domain struct using ToDomain() to pass only domain data to our core service.
I don't like to pass DTO to service layer just to our service layer know only the domain data. If one day your presenter layer change your service layer won't be affected.
But we have a little problem right now, our HandleError don't know how to deal with ErrDecodeJson.. let's see what happen when we dispatch a invalid json to our handler.
curl -X POST http://localhost:8080/v1/books \
-H 'Content-Type: application/json' \
-d '{"title":"title","author":"author","numberPages":100,}'
{"description":"Internal error, please report to admin"}
That's not the best message to say json problem, for this reason we will register our ErrDecodeJson to our map.
To a better maintainability we will move the ErrDecodeJson to our http helper package.
//cmd/http/helper/error.go
var (
errorHandlerMap = make(map[error]int)
ErrDecodeJson = errors.New("error decoding json")
)
// register how to deal with errors here to HTTP layer
func init() {
errorHandlerMap[ErrDecodeJson] = http.StatusBadRequest
}
...
Now we initialize our errorHandlerMap to know how to deal with ErrDecodeJson.
Let's see what happens when we dispatch the same request again.
curl -v -X POST http://localhost:8080/v1/books \ 15:28:03
-H 'Content-Type: application/json' \
-d '{"title":"title","author":"author","numberPages":100,}'
< HTTP/1.1 400 Bad Request
{"description":"error decoding json"}%
Wow, now we explain to the user what error happens and send a Bad Request HTTP Error.
To finish, we will improve our decode process and the response only http code to helper package.
//cmd/http/helper/request.go
package helper
import (
"encoding/json"
"net/http"
)
func BindJson(r *http.Request, destination interface{}) error {
e := json.NewDecoder(r.Body).Decode(destination)
if e != nil {
return ErrDecodeJson
}
return nil
}
and
//cmd/http/helper/response.go
func Response(w http.ResponseWriter, status int) {
addDefaultHeaders(w)
w.WriteHeader(status)
}
Now let's see how our create method looks like.
//cmd/http/handler/book.go
func (h *BookHandler) create(w http.ResponseWriter, r *http.Request) {
dto := presenter.BookPersist{}
if err := helper.BindJson(r, &dto); err != nil {
helper.HandleError(w, err)
return
}
if _, err := h.bookService.Save(dto.ToDomain()); err != nil {
helper.HandleError(w, err)
} else {
helper.Response(w, http.StatusCreated)
}
}
In the next post we will include some validations to our BookPersist and improve our inputs data to not receive a lot of invalid datas.
Top comments (2)
Hi, I'm really enjoying the content, when do you intend to continue?
Hi @eakira i will, unfortunately my little boy passing some problems and i need to focus him. But in June i will start to write again.