DEV Community

Cover image for Creating book data
Marcos Filho
Marcos Filho

Posted on

Creating book data

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)
}
Enter fullscreen mode Exit fullscreen mode

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,
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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

}

...
Enter fullscreen mode Exit fullscreen mode

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"}%
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

and

//cmd/http/helper/response.go

func Response(w http.ResponseWriter, status int) {
    addDefaultHeaders(w)
    w.WriteHeader(status)
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
eakira profile image
eakira

Hi, I'm really enjoying the content, when do you intend to continue?

Collapse
 
maaarkin profile image
Marcos Filho

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.