Webserver part-4
For creating objects using REST api, we either use POST or PUT
-
/GET/ retrieves a representation of the resource at the specified URI. The body of the response message contains the details of the requested resource.
-
/POST/ creates a new resource at the specified URI. The body of the request message provides the details of the new resource. Note that POST can also be used to trigger operations that don’t actually create resources.
-
/PUT/ either creates or replaces the resource at the specified URI. The body of the request message specifies the resource to be created or updated.
-
/PATCH/ performs a partial update of a resource. The request body specifies the set of changes to apply to the resource.
-
/DELETE/ removes the resource at the specified URI. Source: https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design
Use POST for creating new resources and use PUT to update resources.
Adding POST method /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) {
if r.Method == http.MethodGet {
p.getProducts(rw, r)
return
}
if r.Method == http.MethodPost {
p.addProduct(rw, r)
return
}
//catch all
rw.WriteHeader(http.StatusMethodNotAllowed)
}
func (p *Products) getProducts(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)
}
}
func (p *Products) addProduct(rw http.ResponseWriter, r *http.Request) {
p.l.Println("handle POST Product")
}
Trying to curl with POST method using a verbose, -v option
curl -v localhost:8080 -d '{}'| 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)
> POST / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.81.0
> Accept: */*
> Content-Length: 2
> Content-Type: application/x-www-form-urlencoded
>
} [2 bytes data]
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 15 Aug 2024 18:36:59 GMT
< Content-Length: 0
<
100 2 0 0 100 2 0 402 --:--:-- --:--:-- --:--:-- 500
* Connection #0 to host localhost left intact
We next need to convert the POST data into product object https://pkg.go.dev/encoding/json#Decoder
/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:"-"`
}
func (p *Product) FromJSON(r io.Reader) error {
e := json.NewDecoder(r)
return e.Decode(p)
}
// 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(),
},
}
/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) {
if r.Method == http.MethodGet {
p.getProducts(rw, r)
return
}
if r.Method == http.MethodPost {
p.addProduct(rw, r)
return
}
//catch all
rw.WriteHeader(http.StatusMethodNotAllowed)
}
func (p *Products) getProducts(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)
}
}
func (p *Products) addProduct(rw http.ResponseWriter, r *http.Request) {
p.l.Println("handle POST Product")
prod := &data.Product{}
err := prod.FromJSON(r.Body)
if err != nil {
http.Error(rw, "Unable to Unmarshal json", http.StatusBadRequest)
}
p.l.Printf("Prod: %#v", prod)
}
On running the server and passing apost request: curl -v localhost:8080 -d ‘{“id”: 3, “name”: “frapuchinno”, “description”: “nice cup of tea”}’ | jq |
We get the following reqauet on the server:
product-api2024/08/16 00:44:57 Prod: &data.Product{ID:3, Name:"frapuchinno", Description:"nice cup of tea", Price:0, SKU:"", CreatedOn:"", UpdatedOn:"", DeletedOn:""}
Next we will store the recieved data
/data/products.go
//add these lines
func AddProduct(p *Product) {
p.ID = getNextID()
productList = append(productList, p)
}
func getNextID() int {
lp := productList[len(productList)-1]
return lp.ID + 1
}
/handlers/products.go
//add these lines
func (p *Products) addProduct(rw http.ResponseWriter, r *http.Request) {
p.l.Println("handle POST Product")
prod := &data.Product{}
err := prod.FromJSON(r.Body)
if err != nil {
http.Error(rw, "Unable to Unmarshal json", http.StatusBadRequest)
}
//p.l.Printf("Prod: %#v", prod)
data.AddProduct(prod)
}
Now when we send json data, The data is recieved on to the server, nect we would like it to be store. We would use PUT for this.
/data/products.go
package data
import (
"encoding/json"
"fmt"
"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:"-"`
}
func (p *Product) FromJSON(r io.Reader) error {
e := json.NewDecoder(r)
return e.Decode(p)
}
// 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
}
func AddProduct(p *Product) {
p.ID = getNextID()
productList = append(productList, p)
}
func UpdateProduct(id int, p *Product) error {
_, pos, err := findProduct(id)
if err != nil {
return err
}
p.ID = id
productList[pos] = p
return nil
}
var ErrProductNotFound = fmt.Errorf("Product not found")
func findProduct(id int) (*Product, int, error) {
for i, p := range productList {
if p.ID == id {
return p, i, nil
}
}
return nil, -1, ErrProductNotFound
}
func getNextID() int {
lp := productList[len(productList)-1]
return lp.ID + 1
}
// 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(),
},
}
/handlers/products.go
package handlers
import (
"log"
"net/http"
"regexp"
"strconv"
"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) {
// handle the request for a list of products
if r.Method == http.MethodGet {
p.getProducts(rw, r)
return
}
if r.Method == http.MethodPost {
p.addProduct(rw, r)
return
}
if r.Method == http.MethodPut {
p.l.Println("PUT", r.URL.Path)
// expect the id in the URI
reg := regexp.MustCompile(`/([0-9]+)`)
g := reg.FindAllStringSubmatch(r.URL.Path, -1)
if len(g) != 1 {
p.l.Println("Invalid URI more than one id")
http.Error(rw, "Invalid URI", http.StatusBadRequest)
return
}
if len(g[0]) != 2 {
p.l.Println("Invalid URI more than one capture group")
http.Error(rw, "Invalid URI", http.StatusBadRequest)
return
}
idString := g[0][1]
id, err := strconv.Atoi(idString)
if err != nil {
p.l.Println("Invalid URI unable to convert to numer", idString)
http.Error(rw, "Invalid URI", http.StatusBadRequest)
return
}
p.updateProducts(id, rw, r)
return
}
// catch all
// if no method is satisfied return an error
rw.WriteHeader(http.StatusMethodNotAllowed)
}
func (p *Products) getProducts(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)
}
}
func (p *Products) addProduct(rw http.ResponseWriter, r *http.Request) {
p.l.Println("handle POST Product")
prod := &data.Product{}
err := prod.FromJSON(r.Body)
if err != nil {
http.Error(rw, "Unable to Unmarshal json", http.StatusBadRequest)
}
//p.l.Printf("Prod: %#v", prod)
data.AddProduct(prod)
}
func (p Products) updateProducts(id int, rw http.ResponseWriter, r *http.Request) {
p.l.Println("handle PUT Product")
prod := &data.Product{}
err := prod.FromJSON(r.Body)
if err != nil {
http.Error(rw, "Unable to Unmarshal json", http.StatusBadRequest)
}
//p.l.Printf("Prod: %#v", prod)
err = data.UpdateProduct(id, prod)
if err == data.ErrProductNotFound {
http.Error(rw, "Product not found", http.StatusNotFound)
return
}
if err != nil {
http.Error(rw, "Product not found", http.StatusInternalServerError)
return
}
}