From Zero to Go Hero: Learn Go Programming with Me - Part 5
Finishing CRUD API Project
Introduction
Welcome to the last part of this Learn Go Programming series. In the previous parts, we learned lots of new stuff about Go and saw how easy and fast it is to build a backend. In the last blog, we created a Read function for getting all the books that are there in the database.
In this part, we are going to implement the remaining operations which are:
Create Book
Update Book
Delete Book and
Get a Specific Book
So let’s get started without wasting any more time.
Create a Book (POST)
Setting up the route
Now to create a book, first, let’s configure the route for it.
Go to
routes.go
and add a new handler calledCreateBookHandler
// routes.go
func SetupRouter() *chi.Mux {
...
r.Post("/book", books.CreateBookHandler) // Yet to be created inside handler.go
...
}
- As we need to create something inside the database, we need to use the POST method of HTTP.
Creating a Handler
- Head over to
handler.go
and paste the below code:
// handler.go
...
func CreateBookHandler(w http.ResponseWriter, r *http.Request) {
var book Book
err := json.NewDecoder(r.Body).Decode(&book)
if err != nil {
http.Error(w, "Failed to decode book", http.StatusInternalServerError)
return
}
err = CreateBook(book) // Yet to be created inside service.go
if err != nil {
http.Error(w, "Failed to create book", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}
...
So when we are creating something, we are most probably getting the data from the client side. In our case where we are creating a book, let’s say the admin wants to add a book to the website. What he will do is, add the details of the book in text fields on the front side and then press the Submit button.
So when he does that, all the data that were filled by the admin comes to the backend inside a request Body in a JSON format
So what we need to do here is, we just need to
decode
this data into our Go structure format.For that, we used this
json.Decoder()
function which takesjson
string and thenDecode()
takes a struct in which we need to fill the converted data.Then we need to call
CreateBook()
service of database which will add a new entry for the new book.Let’s write this function inside
service.go
Creating a CreateBook
Service
func CreateBook(book Book) error {
_, err := database.DB.Exec("INSERT INTO books (title, author, year) VALUES (?, ?, ?)", book.Title, book.Author, book.Year)
if err != nil {
return err
}
return nil
}
In
service.go
file we need to create a function calledCreateBook
. This function takes a struct of type Book which we will pass from the handler. And it will return an error.Inside the function, we are writing a create query for MySQL database. It’s self-explanatory so I am not going into details.
In the end, we are returning an error if anything goes wrong.
It was pretty simple, right?
Let’s run the project and test it inside Postman.
- I already created two books for testing before so you are also seeing those in output.
Update a Book (PUT)
Setting up the Route
- Now that we have created a book. We can now create an Update functionality. Let’s go to the
routes.go
and create a PUT router
...
r.Put("/book/{id}", books.UpdateBookHandler)
...
We need an
id
to update a particular book, right? To do that we can use a unique identifier which isid
for our book. So When the user/admin updates a particular book client sends theid
of it with the request to our backend.So if you see, after
/book/
we haveid
inside curly braces. This is a dynamic value. When a client hits a URL with this path like,/book/1
or/book/20
, we can get this book dynamicid
and perform update operations.Let’s now create
UpdateBook
handler function
Creating a Handler
func UpdateBookHandler(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
http.Error(w, "Failed to convert id", http.StatusInternalServerError)
return
}
var book Book
err = json.NewDecoder(r.Body).Decode(&book)
if err != nil {
http.Error(w, "Failed to decode book", http.StatusInternalServerError)
return
}
err = UpdateBook(id, book)
if err != nil {
http.Error(w, "Failed to update book", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
Okay so in the first line, we are getting
id
using request objectsPathValue
function. Here we pass the exact value that we passed inside the route which isid
.But we need to convert them
id
toint
because our database accepts the ID of type integer.For that, we are using
strconv
package. And it hasAtoi
(ASCII to Integer) function which takes a string and converts it to an integer. It also returnserr
if it isn’t able to convert. So we are handling that error too.Then we decode the sent JSON data to our book struct.
In the end, we call the
UpdateBook
a function where we pass theid
andbook
struct.Let’s create a database service for Updating a book in the database.
Creating a UpdateBook
Service
func UpdateBook(id int, book Book) error {
_, err := database.DB.Exec("UPDATE books SET title = ?, author = ?, year = ? WHERE id = ?", book.Title, book.Author, book.Year, id)
if err != nil {
return err
}
return nil
}
Here we are just executing an update query.
Let’s run the project and see if it’s working.
Okay, so I changed my mind and thought to give you some exercise.
I want you to create
DELETE
a route for deleting a book. AndGET
route for getting “a single book“ from the database.I will share the code with you but before that, I recommend trying it out on your own.
No cheating…
.
.
.
.
.
- You didn’t try, didn’t you…
- Anyway here is the code..
Deleting a Book
// routes.go
r.Delete("/book/{id}", books.DeleteBookHandler)
// handler.go
func DeleteBookHandler(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
http.Error(w, "Failed to convert id", http.StatusInternalServerError)
return
}
err = DeleteBook(id)
if err != nil {
http.Error(w, "Failed to delete book", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
//service.go
func DeleteBook(id int) error {
_, err := database.DB.Exec("DELETE FROM books WHERE id = ?", id)
if err != nil {
return err
}
return nil
}
Getting Book by id
// routes.go
...
r.Get("/book/{id}", books.GetBookHandler)
...
//handler.go
func GetBookHandler(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
http.Error(w, "Failed to convert id", http.StatusInternalServerError)
return
}
book, err := GetBook(id)
if err != nil {
http.Error(w, "Failed to fetch book", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(book)
}
// service.go
func GetBook(id int) (Book, error) {
var book Book
err := database.DB.QueryRow("SELECT id, title, author, year FROM books WHERE id = ?", id).Scan(&book.ID, &book.Title, &book.Author, &book.Year)
if err != nil {
return book, err
}
return book, nil
}
Modifications
We did finish our project but there are a few things that need to be fixed in this project. For example, When we are Updating a book, we must pass the whole object with every value instead of just a value that we need to update.
There are no clear messages in the console after we perform operations.
We should add proper logs inside each error-handling block.
I want you to do a little bit of research on these points and update the code.
It is pretty easy and I think with a little bit of research and work you will be able to complete it.
GitHub Repository
- Here is the Git repository that contains all the code.
Wrapping Up
That’s it. We have officially completed this series. I hope you learned something from this series.
If you have any queries or concerns regarding any concepts, please feel free to comment it. I’ll try to answer it as per my knowledge.
In future blogs, I am planning to experiment more with Golang and create new projects with it. So stay tuned and follow to get the notifications.
See you in the next one, until then…