Webserver part-3
Writing REST Apis REST: (Representational State Transfer) It as an architectuctural approach for designing web services proposed by Roy Fielding in 2000. Also known as json over http.
We will make apis for an ecommerce coffee shop. We create a data directory and a file called products.go
package data
import (
"time"
)
// Product defines the structure for an API product
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float32 `json:"price"`
SKU string `json:"sku"`
CreatedOn string `json:"-"`
UpdatedOn string `json:"-"`
DeletedOn string `json:"-"`
}
var productList = []*Product{
&Product{
ID: 1,
Name: "Latte",
Description: "Frothy milky coffee",
Price: 2.45,
SKU: "abc323",
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
&Product{
ID: 2,
Name: "Espresso",
Description: "Short and strong coffee without milk",
Price: 1.99,
SKU: "fjd34",
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
}
We crate a new handler, products.go
package handlers
type Products struct {
l *log.Logger
}
func NewProducts(l *log.Logger) *Products {
return &Products{l}
}
func (p *Products) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
//
}
/data/products.go
package data
import (
"time"
)
// Product defines the structure for an API product
type Product struct {
ID int
Name string
Description string
Price float32
SKU string
CreatedOn string
UpdatedOn string
DeletedOn string
}
// GetProduct functions returns list of products.
func GetProducts() []Product{
return productList
}
var productList = []*Product{
&Product{
ID: 1,
Name: "Latte",
Description: "Frothy milky coffee",
Price: 2.45,
SKU: "abc323",
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
&Product{
ID: 2,
Name: "Espresso",
Description: "Short and strong coffee without milk",
Price: 1.99,
SKU: "fjd34",
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
}
We use json encoding. /handlers/products.go
package handlers
import (
"encoding/json"
"log"
"net/http"
"github.com/dileepkushwaha/server/data"
)
type Products struct {
l *log.Logger
}
func NewProducts(l *log.Logger) *Products {
return &Products{l}
}
func (p *Products) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
lp := data.GetProducts()
d, err := json.Marshal(lp)
if err != nil {
http.Error(rw, "Unable to marshal json", http.StatusInternalServerError)
}
rw.Write(d)
}
While running curl command the following output is obtained.
curl localhost:8080 | jq
[
{
"ID": 1,
"Name": "Latte",
"Description": "Frothy milky coffee",
"Price": 2.45,
"SKU": "abc323",
"CreatedOn": "2024-08-15 10:55:12.207505324 +0000 UTC",
"UpdatedOn": "2024-08-15 10:55:12.207508393 +0000 UTC",
"DeletedOn": ""
},
{
"ID": 2,
"Name": "Espresso",
"Description": "Short and strong coffee without milk",
"Price": 1.99,
"SKU": "fjd34",
"CreatedOn": "2024-08-15 10:55:12.20750907 +0000 UTC",
"UpdatedOn": "2024-08-15 10:55:12.207509536 +0000 UTC",
"DeletedOn": ""
}
]
We will be suing json annotations inorder to display data according to our needs. /data/products.go
package data
import (
"encoding/json"
"io"
"time"
)
// Product defines the structure for an API product
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float32 `json:"price"`
SKU string `json:"sku"`
CreatedOn string `json:"-"`
UpdatedOn string `json:"-"`
DeletedOn string `json:"-"`
}
// GetProducts returns a list of products
func GetProducts() Products {
return productList
}
// productList is a hard coded list of products for this
// example data source
var productList = []*Product{
&Product{
ID: 1,
Name: "Latte",
Description: "Frothy milky coffee",
Price: 2.45,
SKU: "abc323",
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
&Product{
ID: 2,
Name: "Espresso",
Description: "Short and strong coffee without milk",
Price: 1.99,
SKU: "fjd34",
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
}
Now we get the output like we wanted to.
$curl localhost:8080 | jq
[
{
"id": 1,
"name": "Latte",
"description": "Frothy milky coffee",
"price": 2.45,
"sku": "abc323"
},
{
"id": 2,
"name": "Espresso",
"description": "Short and strong coffee without milk",
"price": 1.99,
"sku": "fjd34"
}
]
The Func NewEcoder instead of returning a slice of data and an error, it writes the output to the io.writer,
func NewEncoder(w io.Writer) *Encoder
Using Encoder, we dont have to allocated buffer in the memory. Having large json object can slow things down using Marshalling technique. Encoders are relatively faster.
package data
import (
"encoding/json"
"io"
"time"
)
// Product defines the structure for an API product
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float32 `json:"price"`
SKU string `json:"sku"`
CreatedOn string `json:"-"`
UpdatedOn string `json:"-"`
DeletedOn string `json:"-"`
}
// Products is a collection of Product
type Products []*Product
// ToJSON serializes the contents of the collection to JSON
// NewEncoder provides better performance than json.Unmarshal as it does not
// have to buffer the output into an in memory slice of bytes
// this reduces allocations and the overheads of the service
//
// https://golang.org/pkg/encoding/json/#NewEncoder
func (p *Products) ToJSON(w io.Writer) error {
e := json.NewEncoder(w)
return e.Encode(p)
}
// GetProducts returns a list of products
func GetProducts() [ ]*Products {
return productList
}
// productList is a hard coded list of products for this
// example data source
var productList = []*Product{
&Product{
ID: 1,
Name: "Latte",
Description: "Frothy milky coffee",
Price: 2.45,
SKU: "abc323",
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
&Product{
ID: 2,
Name: "Espresso",
Description: "Short and strong coffee without milk",
Price: 1.99,
SKU: "fjd34",
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
}
Refactoring /handlers.products.go
package handlers
import (
"log"
"net/http"
"github.com/dileepkushwaha/server/data"
)
type Products struct {
l *log.Logger
}
func NewProducts(l *log.Logger) *Products {
return &Products{l}
}
func (p *Products) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
lp := data.GetProducts()
err := lp.ToJSON(rw)
if err != nil {
http.Error(rw, "Unable to marshal json", http.StatusInternalServerError)
}
}
/data/products.go
package data
import (
"encoding/json"
"io"
"time"
)
// Product defines the structure for an API product
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float32 `json:"price"`
SKU string `json:"sku"`
CreatedOn string `json:"-"`
UpdatedOn string `json:"-"`
DeletedOn string `json:"-"`
}
// Products is a collection of Product
type Products []*Product
// ToJSON serializes the contents of the collection to JSON
// NewEncoder provides better performance than json.Unmarshal as it does not
// have to buffer the output into an in memory slice of bytes
// this reduces allocations and the overheads of the service
//
// https://golang.org/pkg/encoding/json/#NewEncoder
func (p *Products) ToJSON(w io.Writer) error {
e := json.NewEncoder(w)
return e.Encode(p)
}
// GetProducts returns a list of products
func GetProducts() Products {
return productList
}
// productList is a hard coded list of products for this
// example data source
var productList = []*Product{
&Product{
ID: 1,
Name: "Latte",
Description: "Frothy milky coffee",
Price: 2.45,
SKU: "abc323",
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
&Product{
ID: 2,
Name: "Espresso",
Description: "Short and strong coffee without milk",
Price: 1.99,
SKU: "fjd34",
CreatedOn: time.Now().UTC().String(),
UpdatedOn: time.Now().UTC().String(),
},
}
How do we read json? Implement Get and Post requests:
Refactoring adding getpProduct internal function
func (p *Products) getProducts(rw http.ResponseWriter, h *http.Request) {
lp := data.GetProducts()
err := lp.ToJSON(rw)
if err != nil {
http.Error(rw, "Unable to marshal json", http.StatusInternalServerError)
}
}
Uptill Golang 1.19, Go standard library doesn’t have provision for various REST verbs management. People optede to use frameworks like Gin and GorillaMux.
Implementing REST verb management in vanilla golang
/handlers/products.go
func (p *Products) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
p.getProducts(rw, r)
return
}
//catch all
rw.WriteHeader(http.StatusMethodNotAllowed)
}
When Get method is curled, reponse is same. WWhen a Delete method is used, Error messege is recieved.
curl localhost:8080 -XDELETE -v |jq
* Trying 127.0.0.1:8080...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Connected to localhost (127.0.0.1) port 8080 (#0)
> DELETE / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 405 Method Not Allowed
< Date: Thu, 15 Aug 2024 13:17:41 GMT
< Content-Length: 0
<
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
* Connection #0 to host localhost left intact