Hey everyone, Welcome back. If you’re new to this series of blogs, have a look on Part I which talks about What are Microservices and Getting started with REST API’s in Go. In this blog, we will continue on our learning journey and implement GET(single), PUT and DELETE API’s on our product catalog server for an e-commerce site.
Before we go ahead and implement new API’s on our server, we will refactor the existing code a bit to make it more organized and reusable. If you see in the current code for our handlers, all the business logic and protocol (http) related stuff are bind together. Let’s refactor this.
Refactoring Existing Code

Not that tough though. Let’s go ahead and start refactoring 💪.
Go to our product.go file and update the code with the following code. We are moving the product related logic to our product file, rather than doing everything in the handler itself.
package entity
import (
"encoding/json"
"errors"
"io/ioutil"
"os"
)
//Product defines a structure for an item in product catalog
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
IsAvailable bool `json:"isAvailable"`
}
func GetProducts() ([]byte, error) {
// Read JSON file
data, err := ioutil.ReadFile("./data/data.json")
if err != nil {
return nil, err
}
return data, nil
}
func AddProduct(product Product) error {
// Load existing products and append the data to product list
var products []Product
data, err := ioutil.ReadFile("./data/data.json")
if err != nil {
return err
}
// Load our JSON file to memory using array of products
err = json.Unmarshal(data, &products)
if err != nil {
return err
}
// Add new Product to our list
products = append(products, product)
// Write Updated JSON file
updatedData, err := json.Marshal(products)
if err != nil {
return err
}
err = ioutil.WriteFile("./data/data.json", updatedData, os.ModePerm)
if err != nil {
return err
}
return nil
}
If you carefully observe, we have moved the logic which deals with our JSON file from handlers.go to product.go. So let’s go ahead and update handlers.go accordingly. For simplicity, I am mentioning the function body which needs update.
// GetProductsHandler is used to get data inside the products defined on our product catalog
func GetProductsHandler() http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
data, err := entity.GetProducts()
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
// Write the body with JSON data
rw.Header().Add("content-type", "application/json")
rw.WriteHeader(http.StatusFound)
rw.Write(data)
}
}
// CreateProductHandler is used to create a new product and add to our product store.
func CreateProductHandler() http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
// Read incoming JSON from request body
data, err := ioutil.ReadAll(r.Body)
// If no body is associated return with StatusBadRequest
if err != nil {
rw.WriteHeader(http.StatusBadRequest)
return
}
// Check if data is proper JSON (data validation)
var product entity.Product
err = json.Unmarshal(data, &product)
if err != nil {
rw.WriteHeader(http.StatusExpectationFailed)
rw.Write([]byte("Invalid Data Format"))
return
}
err = entity.AddProduct(product)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
// return after writing Body
rw.WriteHeader(http.StatusCreated)
rw.Write([]byte("Added New Product"))
}
}
Our code seems pretty clean comparatively. Go ahead and check if the refactored code still works by starting our server and test it on POSTMAN.
go run main.go
Here it is, testing our refactored code on postman.

It’s still working, we haven’t messed up anything in the name of refactoring😜💯

Adding New API’s 🙌
We have seen GET all and Create a product in our previous blog. In this blog, we will add the functionality to get a desired product, delete a product and also update one. Let’s get started.
Adding Business Logic
This time, without the need of refactoring we will add the logic deals with data in product.go file itself. Update the existing file with the following code, which now has got functions to delete a product and get a single product given it’s id as input.
package entity
import (
"encoding/json"
"errors"
"io/ioutil"
"os"
)
//Product defines a structure for an item in product catalog
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
IsAvailable bool `json:"isAvailable"`
}
// ErrNoProduct is used if no product found
var ErrNoProduct = errors.New("no product found")
// GetProducts returns the JSON file content if available else returns an error.
func GetProducts() ([]byte, error) {
// Read JSON file
data, err := ioutil.ReadFile("./data/data.json")
if err != nil {
return nil, err
}
return data, nil
}
// GetProduct takes id as input and returns the corresponding product, else it returns ErrNoProduct error.
func GetProduct(id string) (Product, error) {
// Read JSON file
data, err := ioutil.ReadFile("./data/data.json")
if err != nil {
return Product{}, err
}
// read products
var products []Product
err = json.Unmarshal(data, &products)
if err != nil {
return Product{}, err
}
// iterate through product array
for i := 0; i < len(products); i++ {
// if we find one product with the given ID
if products[i].ID == id {
// return product
return products[i], nil
}
}
return Product{}, ErrNoProduct
}
// DeleteProduct takes id as input and deletes the corresponding product, else it returns ErrNoProduct error.
func DeleteProduct(id string) error {
// Read JSON file
data, err := ioutil.ReadFile("./data/data.json")
if err != nil {
return err
}
// read products
var products []Product
err = json.Unmarshal(data, &products)
if err != nil {
return err
}
// iterate through product array
for i := 0; i < len(products); i++ {
// if we find one product with the given ID
if products[i].ID == id {
products = removeElement(products, i)
// Write Updated JSON file
updatedData, err := json.Marshal(products)
if err != nil {
return err
}
err = ioutil.WriteFile("./data/data.json", updatedData, os.ModePerm)
if err != nil {
return err
}
return nil
}
}
return ErrNoProduct
}
// AddProduct adds an input product to the product list in JSON document.
func AddProduct(product Product) error {
// Load existing products and append the data to product list
var products []Product
data, err := ioutil.ReadFile("./data/data.json")
if err != nil {
return err
}
// Load our JSON file to memory using array of products
err = json.Unmarshal(data, &products)
if err != nil {
return err
}
// Add new Product to our list
products = append(products, product)
// Write Updated JSON file
updatedData, err := json.Marshal(products)
if err != nil {
return err
}
err = ioutil.WriteFile("./data/data.json", updatedData, os.ModePerm)
if err != nil {
return err
}
return nil
}
// removeElement is used to remove element from product array at given index
func removeElement(arr []Product, index int) []Product {
ret := make([]Product, 0)
ret = append(ret, arr[:index]...)
return append(ret, arr[index+1:]...)
}
In the above piece of code, you can see a new ErrNoProduct error, which is returned by DeleteProduct and GetProduct methods if no ID matches the input ID coming from the user. Please feel free to go through the comments inside code for step by step explanation.
Adding Handlers
Now that we have logic which deals with the required functionality, let’s add handlers to serve that functionality over HTTP. Update your handlers.go with the following code.
package handlers
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"github.com/HelloWorld/goProductAPI/entity"
"github.com/gorilla/mux"
)
// GetProductsHandler is used to get data inside the products defined on our product catalog
func GetProductsHandler() http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
data, err := entity.GetProducts()
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
// Write the body with JSON data
rw.Header().Add("content-type", "application/json")
rw.WriteHeader(http.StatusFound)
rw.Write(data)
}
}
// GetProductHandler is used to get data inside the products defined on our product catalog
func GetProductHandler() http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
// Read product ID
productID := mux.Vars(r)["id"]
product, err := entity.GetProduct(productID)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
responseData, err := json.Marshal(product)
if err != nil {
// Check if it is No product error or any other error
if errors.Is(err, entity.ErrNoProduct) {
// Write Header if no related product found.
rw.WriteHeader(http.StatusNoContent)
} else {
rw.WriteHeader(http.StatusInternalServerError)
}
return
}
// Write body with found product
rw.Header().Add("content-type", "application/json")
rw.WriteHeader(http.StatusFound)
rw.Write(responseData)
}
}
// CreateProductHandler is used to create a new product and add to our product store.
func CreateProductHandler() http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
// Read incoming JSON from request body
data, err := ioutil.ReadAll(r.Body)
// If no body is associated return with StatusBadRequest
if err != nil {
rw.WriteHeader(http.StatusBadRequest)
return
}
// Check if data is proper JSON (data validation)
var product entity.Product
err = json.Unmarshal(data, &product)
if err != nil {
rw.WriteHeader(http.StatusExpectationFailed)
rw.Write([]byte("Invalid Data Format"))
return
}
err = entity.AddProduct(product)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
// return after writing Body
rw.WriteHeader(http.StatusCreated)
rw.Write([]byte("Added New Product"))
}
}
// DeleteProductHandler deletes the product with given ID.
func DeleteProductHandler() http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
// Read product ID
productID := mux.Vars(r)["id"]
err := entity.DeleteProduct(productID)
if err != nil {
// Check if it is No product error or any other error
if errors.Is(err, entity.ErrNoProduct) {
// Write Header if no related product found.
rw.WriteHeader(http.StatusNoContent)
} else {
rw.WriteHeader(http.StatusInternalServerError)
}
return
}
// Write Header with Accepted Status (done operation)
rw.WriteHeader(http.StatusAccepted)
}
}
// UpdateProductHandler deletes the product with given ID.
func UpdateProductHandler() http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
// Read product ID
productID := mux.Vars(r)["id"]
err := entity.DeleteProduct(productID)
if err != nil {
if errors.Is(err, entity.ErrNoProduct) {
rw.WriteHeader(http.StatusNoContent)
} else {
rw.WriteHeader(http.StatusInternalServerError)
}
return
}
// Read incoming JSON from request body
data, err := ioutil.ReadAll(r.Body)
// If no body is associated return with StatusBadRequest
if err != nil {
rw.WriteHeader(http.StatusBadRequest)
return
}
// Check if data is proper JSON (data validation)
var product entity.Product
err = json.Unmarshal(data, &product)
if err != nil {
rw.WriteHeader(http.StatusExpectationFailed)
rw.Write([]byte("Invalid Data Format"))
return
}
// Addproduct with the requested body
err = entity.AddProduct(product)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
// Write Header if no related product found.
rw.WriteHeader(http.StatusAccepted)
}
}
We have added three new handlers to our server, which helps us in GET, DELETE and UPDATE operations for a single product. As always, I have made code self explanatory, so go through the comments for better understanding.
Adding Routes to Handlers
Now that we have handlers ready it’s time we route requests to these handlers appropriately. Let’s go ahead and update main.go file accordingly.
package main
import (
"fmt"
"net/http"
"github.com/HelloWorld/goProductAPI/handlers"
"github.com/gorilla/mux"
)
func main() {
// Create new Router
router := mux.NewRouter()
// route properly to respective handlers
router.Handle("/products", handlers.GetProductsHandler()).Methods("GET")
router.Handle("/products", handlers.CreateProductHandler()).Methods("POST")
router.Handle("/products/{id}", handlers.GetProductHandler()).Methods("GET")
router.Handle("/products/{id}", handlers.DeleteProductHandler()).Methods("DELETE")
router.Handle("/products/{id}", handlers.UpdateProductHandler()).Methods("PUT")
// Create new server and assign the router
server := http.Server{
Addr: ":9090",
Handler: router,
}
fmt.Println("Staring Product Catalog server on Port 9090")
// Start Server on defined port/host.
server.ListenAndServe()
}
We have just added 3 new routes to our mux router. The {id} corresponds to an URL parameter which comes as a named parameter in the request params.
Testing Time
As always, go ahead and start our server using the following command in the root of your project.
go run main.go
We will test our API’s using POSTMAN as we did in our previous blog. So let’s go ahead and test our GET, DELETE and PUT API’s. Here’re the screenshots from the POSTMAN on my machine.
GET (Single Product)

GET for single product is successful, with 302 found and JSON in the response body.
UPDATE (Single Product)

We have updated the price of iPhone Pro from 100000 to 105000 and made a PUT request, it says 202 Accepted and this should reflect in our JSON file. Let’s check it out.

DELETE (Single Product)

We made a request to delete the element with iPhone 12 Pro data using it’s ID as parameter and we have got 202 Accepted as response. Let’s check the JSON file, if the entry is still there.

We see that the corresponding JSON element is deleted from the JSON document. So, everything’s working great until now.

So, that’s it for this blog. You can find the whole code on our GitHub repository. You now know how to implement all the basic CRUD (Create, Read, Update and Delete) operations using REST API’s in GoLang. Congratulations 👏.

What’s next? 🤔
We will add unit tests to our code in our next blog, and also look at externalizing data into a database in our upcoming blogs. Keep learning. We will meet again on our learning journey. Until then, stay safe. Cheers ✌
2 responses to “Microservices in Go – Part 2”
[…] bang! In this blog we will add authentication to the very simple microservice we have developed in our previous blog. Let’s go […]
LikeLike
[…] blog in our Microservices in Go series. Until now, we have seen how to use standard http package to create a REST API in Go. We have also seen how to add basic authentication to our REST […]
LikeLike