This tutorial introduces the basics of writing a RESTful web service API with Go and the Gin Web Framework (Gin). Which uses my two open source libraries: ginvalidator and validatorgo.
- ginvalidator is a lightweight and expressive validation library for Go, designed specifically for the Gin web framework. It is based on the popular open source js library express-validator.
- validatorgo is a versatile validation library for Go, offering a wide range of validators and sanitizers. It is based on the popular open source js library validator.js.
Please check out the above libraries and don’t forget to ⭐ star, 🔁 share, and 🍴 fork!
API endpoint design
You’ll build an API that provides access to a product inventory management system. So you’ll need to provide endpoints through which a HTTP client can create, read update and delete products.
When developing an API, you typically begin by designing the endpoints. Your API’s users will have more success if the endpoints are easy to understand.
Here are the endpoints you’ll create in this tutorial.
-
/products
-
GET
- Get a list of all products, returned as JSON. -
POST
- Add a new product from request data and returned as JSON.
-
-
/products/:id
-
GET
- Get a product by itsid
, returning the product data as JSON. -
UPDATE
- Get a product by itsid
, returning the newly updated data as JSON. -
Delete
- Deletes a product by itsid
.
-
To keep things simple for the tutorial, you’ll store data in memory. A more typical API would interact with a database. Note that storing data in memory means that the list of products will be lost each time you stop the server, then recreated when you start it.
Also all HTTP requests will be represented as .http
code snippets. You can use any HTTP client, such as Postman, Insomnia, or the HTTP client in your IDE, to send your requests.
Writing the Code
Start by following the quick installation guide to set up the necessary dependencies.
Next, create a
main.go
file.In the
main.go
file, define the following structs to model your product data:Dimensions
,Supplier
, andProduct
. TheDimensions
andSupplier
structs will be embedded within theProduct
struct.
package main
import (
"time"
)
// Dimensions represents the physical dimensions of a product.
type Dimensions struct {
Length float64 `json:"length"` // Length of the product in centimeters
Width float64 `json:"width"` // Width of the product in centimeters
Height float64 `json:"height"` // Height of the product in centimeters
Weight float64 `json:"weight"` // Weight of the product in kilograms
}
// Supplier represents information about the supplier of a product.
type Supplier struct {
Name string `json:"name"` // Name of the supplier
Contact string `json:"contact"` // Contact details (e.g., email or phone number)
Address string `json:"address"` // Address of the supplier
}
// Product represents data about an inventory product.
type Product struct {
ID string `json:"id"` // Unique identifier for the product
Name string `json:"name"` // Name of the product
Category string `json:"category"` // Category the product belongs to
Description string `json:"description"` // Detailed description of the product
Price float64 `json:"price"` // Price of the product in decimal format
Stock int `json:"stock"` // Quantity of the product available in stock
Dimensions Dimensions `json:"dimensions"` // Physical dimensions of the product
Supplier Supplier `json:"supplier"` // Supplier details
Tags []string `json:"tags"` // Tags for searching and filtering
Image string `json:"image"` // URLs of product images
ManufacturedAt time.Time `json:"manufacturedAt"` // Timestamp when the product was manufactured
CreatedAt time.Time `json:"createdAt"` // Timestamp when the product was created
UpdatedAt time.Time `json:"updatedAt"` // Timestamp when the product was last updated
}
- Then below add a slice products, with 2 items. This slice will be where our in memory database.
var products = []Product{
{
ID: "p1",
Name: "Wireless Mouse",
Category: "Electronics",
Description: "A high-precision wireless mouse with ergonomic design.",
Price: 29.99,
Stock: 150,
Dimensions: Dimensions{
Length: 11.5,
Width: 6.0,
Height: 3.5,
Weight: 0.2,
},
Supplier: Supplier{
Name: "Tech Supplies Inc.",
Contact: "support@techsupplies.com",
Address: "123 Tech Street, Silicon Valley, CA",
},
Tags: []string{"wireless", "mouse", "electronics", "accessories"},
Image: "https://example.com/images/mouse1.jpg",
ManufacturedAt: time.Now().AddDate(0, -30, 0),
CreatedAt: time.Now().AddDate(0, -1, 0),
UpdatedAt: time.Now().AddDate(0, -1, 0),
},
{
ID: "p2",
Name: "Gaming Keyboard",
Category: "Electronics",
Description: "Mechanical keyboard with customizable RGB lighting.",
Price: 79.99,
Stock: 75,
Dimensions: Dimensions{
Length: 45.0,
Width: 15.0,
Height: 3.8,
Weight: 1.1,
},
Supplier: Supplier{
Name: "Gaming Gear Ltd.",
Contact: "info@gaminggear.com",
Address: "456 Gaming Road, Austin, TX",
},
Tags: []string{"keyboard", "gaming", "RGB", "electronics"},
Image: "https://example.com/images/keyboard2.jpg",
ManufacturedAt: time.Now().AddDate(0, -20, 0),
CreatedAt: time.Now().AddDate(0, -2, 0),
UpdatedAt: time.Now().AddDate(0, -2, 0),
},
}
- Lets register a
GET
endpoint for/products
in themain
function. Here is the updatedmain
function:
func main() {
router := gin.Default()
router.GET("/products",
append(productQueriesValidators(), getProducts)...,
)
router.Run(":8080")
}
Below the main
function, define the productQueriesValidators
and getProducts
functions:
-
productQueriesValidators
: This function returns agin.HandlersChain
(equivalent to[]gin.HandlerFunc
). It includes two query validators for theGET
/products
route:-
q
: An optional query parameter. It will be validated only if present in the request, ensuring it's trimmed of leading and trailing whitespace and is not empty. -
order
: An optional query parameter. It will be trimmed of whitespace and must be one of asc or desc.
-
func productQueriesValidators() gin.HandlersChain {
return gin.HandlersChain{
gv.NewQuery("q", nil).Chain().Optional().Trim(" ").Not().Empty(nil).Validate(),
gv.NewQuery("order", nil).Chain().Optional().Trim(" ").In([]string{"asc", "desc"}).Validate(),
}
}
-
getProducts
: This is the final request handler middleware for the GET/products
route. It performs the following steps:- Checks for validation errors using
gv.ValidationResult
. - If validation errors exist, it returns an HTTP 422 Unprocessable Entity status with the errors.
- If no validation errors occur, it returns the product data with an HTTP 200 OK status.
- Checks for validation errors using
func getProducts(c *gin.Context) {
result, err := gv.ValidationResult(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
if len(result) != 0 {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"message": "The server encountered validation error",
"data": nil,
"errors": result,
})
return
}
c.AbortWithStatusJSON(http.StatusOK, gin.H{
"message": "The products found successfully",
"data": products,
"errors": nil,
})
}
Note we don't actually use the the search params (
q
,order
) in the getProducts handler for the sake of simplicity.
Update the dependencies at the top of your Go file to include both net/http
, gin
and ginvalidator
(aliased as gv
):
package main
import (
"fmt"
"net/http"
"time"
gv "github.com/bube054/ginvalidator"
"github.com/gin-gonic/gin"
)
- Run and test the code for the
GET
/products
endpoint.
go run main.go
Here are examples of valid and invalid requests for testing:
-
A valid request.
GET http://localhost:8080/products?q=electronics&order=asc HTTP/1.1 content-type: application/json
-
An Invalid Request:
order
is neitherasc
nordesc
.
GET http://localhost:8080/products?q=&order=xyz HTTP/1.1 content-type: application/json
-
An Invalid Request:
q
is empty
GET http://localhost:8080/products?q= &order=asc HTTP/1.1 content-type: application/json
-
An Invalid Request:
q
is empty andorder
is neitherasc
nordesc
GET http://localhost:8080/products?q= &order=xyz HTTP/1.1 content-type: application/json
- Lets register a
GET
endpoint for/products/:id
in themain
function. Here is the updatedmain
function:
- Lets register a
func main() {
router := gin.Default()
router.GET("/products",
append(productQueriesValidators(), getProducts)...,
)
router.GET("/products/:id",
append(productParamIdValidators(), getProduct)...,
)
router.Run(":8080")
}
Below the main
function, define the productParamIdValidators
and getProduct
functions:
-
productParamIdValidators
: It includes a param validator forid
in/products/:id
route:-
id
is simply the id for each product.
-
func productParamIdValidators() gin.HandlersChain {
return gin.HandlersChain{gv.NewParam("id", nil).Chain().Trim(" ").Alphanumeric(nil).Validate()}
}
-
getProducts
: This is the final request handler middleware for the/products
route. It performs the following steps- Checks for validation errors using
gv.ValidationResult
. - If validation errors exist, it returns an HTTP 422 Unprocessable Entity status with the errors.
- Retrieves matched data using
gv.GetMatchedData
. - Searches for the product by its id in the products list.
- Return HTTP 404 if the product is not found and return HTTP 200 if the product is successfully found.
- Checks for validation errors using
func getProduct(c *gin.Context) {
result, err := gv.ValidationResult(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
if len(result) != 0 {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"message": "The server encountered validation error",
"data": nil,
"errors": result,
})
return
}
data, err := gv.GetMatchedData(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
id, _ := data.Get(gv.ParamLocation, "id")
var product *Product
for _, p := range products {
if p.ID == id {
product = &p
break
}
}
if product == nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"message": fmt.Sprintf("Product with id %s, not found", id),
"data": nil,
"errors": nil,
})
return
} else {
c.AbortWithStatusJSON(http.StatusOK, gin.H{
"message": "The product found successfully",
"data": product,
"errors": nil,
})
return
}
}
- Run and test the code for GET on
/products/:id
Quit the previous process and rerun:
go run main.go
A valid request.
GET http://localhost:8080/products/p2 HTTP/1.1
content-type: application/json
Invalid request: id
which is p2
is not alphanumeric
GET http://localhost:8080/products/@#! HTTP/1.1
content-type: application/json
- Lets register a
DELETE
endpoint for/products/:id
in themain
function. Here is the updatedmain
function:
func main() {
router := gin.Default()
router.GET("/products",
append(productQueriesValidators(), getProducts)...,
)
router.GET("/products/:id",
append(productParamIdValidators(), getProduct)...,
)
router.DELETE("/products/:id",
append(productParamIdValidators(), deleteProduct)...,
)
router.Run(":8080")
}
productParamIdValidators
function will be reused.
-
deleteProduct
: This is the final request handler middleware for theDELETE
/products/:id
route. It performs the following steps:- Validates the request parameters using
gv.ValidationResult
. - Returns an HTTP 422 status if validation errors are present.
- Retrieves matched data using
gv.GetMatchedData
. - Searches for the product by its id in the products list.
- If the product exists, its removed from the list and returns HTTP 200 with the deleted product details.
- If the product does not exist, returns HTTP 404.
- Validates the request parameters using
func deleteProduct(c *gin.Context) {
result, err := gv.ValidationResult(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
if len(result) != 0 {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"message": "The server encountered validation error",
"data": nil,
"errors": result,
})
return
}
data, err := gv.GetMatchedData(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": "The server encountered an unexpected error.",
"data": nil,
"errors": nil,
})
return
}
id, _ := data.Get(gv.ParamLocation, "id")
var deletedProduct *Product
var filteredProducts = []Product{}
for _, prod := range products {
if prod.ID == id {
deletedProduct = &prod
break
} else {
filteredProducts = append(filteredProducts, prod)
}
}
if deletedProduct == nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"message": fmt.Sprintf("Product with id %s, not found", id),
"data": nil,
"errors": nil,
})
return
}
products = filteredProducts
c.AbortWithStatusJSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Product with id %s, deleted", id),
"data": deletedProduct,
"errors": nil,
})
}
- Run and test the code for
DELETE
/products/:id
Quit the previous process and rerun:
go run main.go
A valid request.
DELETE http://localhost:8080/products/p2 HTTP/1.1
content-type: application/json
Invalid request: id
which is p2
is not alphanumeric
DELETE http://localhost:8080/products/@#! HTTP/1.1
content-type: application/json
- Lets register a
POST
endpoint for/products
in themain
function. Here is the updatedmain
function:
func main() {
router := gin.Default()
router.GET("/products",
append(productQueriesValidators(), getProducts)...,
)
router.GET("/products/:id",
append(productParamIdValidators(), getProduct)...,
)
router.DELETE("/products/:id",
append(productParamIdValidators(), deleteProduct)...,
)
router.POST("/products",
append(productBodyValidators(), postProduct)...,
)
router.Run(":8080")
}
-
productBodyValidators
: This function validates the request body forPOST
andPUT
requests to the/products
route. It validates:-
name
: Must not be empty. -
category
: Must belong to predefined categories (Electronics, Apparels, Groceries, Home-Appliances). -
description
: Length should be between 5 and 100 characters. -
price
: Must have at least 2 decimal places and use the en-US locale. -
stock
: Should be an integer, with a minimum value of 0. -
dimensions
:-
length
,width
,height
,weight
: Must be numeric.
-
-
supplier
:-
name
: Trimmed and not empty. -
contact
: Must be a valid email address. -
address
: Must match the regex format (number street, city, state abbreviation).
-
-
tags
: Should be an array of strings. -
image
: Must be a valid URL. -
manufacturedAt
: Should follow theISO8601
format and be after2020-05-10T00:00:00Z
.
-
func productBodyValidators() gin.HandlersChain {
var (
minDesc uint = 5
maxDesc uint = 100
minStock int = 0
validCategories []string = []string{"Electronics", "Apparels", "Groceries", "Home-Appliances"}
)
return gin.HandlersChain{
gv.NewBody("name", nil).Chain().Not().Empty(nil).Validate(),
gv.NewBody("category", nil).Chain().In(validCategories).Validate(),
gv.NewBody("description", nil).Chain().Length(&vgo.IsLengthOpts{Min: uint(minDesc), Max: &maxDesc}).Validate(),
gv.NewBody("price", nil).Chain().Decimal(&vgo.IsDecimalOpts{DecimalDigits: vgo.DecimalDigits{Min: 2}, ForceDecimal: false, Locale: "en-US"}).Validate(),
gv.NewBody("stock", nil).Chain().Int(&vgo.IsIntOpts{Min: &minStock}).Validate(),
gv.NewBody(`dimensions.length`, nil).Chain().Numeric(nil).Validate(),
gv.NewBody("dimensions.width", nil).Chain().Numeric(nil).Validate(),
gv.NewBody("dimensions.height", nil).Chain().Numeric(nil).Validate(),
gv.NewBody("dimensions.weight", nil).Chain().Numeric(nil).Validate(),
gv.NewBody("supplier.name", nil).Chain().Trim(" ").Not().Empty(nil).Validate(),
gv.NewBody("supplier.contact", nil).Chain().Email(nil).Validate(),
gv.NewBody("supplier.address", nil).Chain().Matches(regexp.MustCompile(`^\d+\s[\w\s]+,\s[\w\s]+,\s[A-Z]{2}$`)).Validate(),
gv.NewBody("tags", nil).Chain().Array(nil).Validate(),
gv.NewBody("image", nil).Chain().URL(nil).Validate(),
gv.NewBody("manufacturedAt", nil).Chain().Date(&vgo.IsDateOpts{Format: vgo.ISO8601ZuluLayout, StrictMode: true}).After(&vgo.IsAfterOpts{ComparisonDate: "2020-05-10T00:00:00Z"}).Validate(),
}
}
-
postProduct
: This is the final request handler middleware for thePOST
/products
route. It performs the following steps:- Validates the request body using
gv.ValidationResult
. - Returns an HTTP 422 status if validation errors are present.
- Binds the JSON request body to a Product struct.
- Adds a unique ID, CreatedAt, and UpdatedAt fields to the new product.
- Appends the new product to the products slice.
- Returns HTTP 200 with the created product details.
- Validates the request body using
func postProduct(c *gin.Context) {
result, err := gv.ValidationResult(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
if len(result) != 0 {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"message": "The server encountered validation error",
"data": nil,
"errors": result,
})
return
}
var newProduct Product
if err := c.BindJSON(&newProduct); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
newProduct.ID = fmt.Sprintf("p%d", len(products)+1)
newProduct.CreatedAt = time.Now()
newProduct.UpdatedAt = newProduct.CreatedAt
products = append(products, newProduct)
c.AbortWithStatusJSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Product with id %s, has been created", newProduct.ID),
"data": newProduct,
"errors": nil,
})
}
Update the dependencies at the top of your Go file to include validatorgo
(aliased as vgo
):
package main
import (
"fmt"
"net/http"
"regexp"
"time"
gv "github.com/bube054/ginvalidator"
vgo "github.com/bube054/validatorgo"
"github.com/gin-gonic/gin"
)
- Run and test the code for
POST
/products
Quit the previous process and rerun:
go run main.go
A valid request.
POST http://localhost:8080/products HTTP/1.1
content-type: application/json
{
"name": "Ultra HD Monitor",
"category": "Electronics",
"description": "27-inch 4K Ultra HD monitor with HDR support and ultra-slim bezels.",
"price": 345.99,
"stock": 30,
"dimensions": {
"length": 61.0,
"width": 36.0,
"height": 5.0,
"weight": 4.5
},
"supplier": {
"name": "VisionTech Co.",
"contact": "sales@visiontech.com",
"address": "123 Display Lane, San Francisco, CA"
},
"tags": ["monitor", "4K", "HDR", "electronics", "display"],
"image": "https://example.com/images/monitor1.jpg",
"manufacturedAt": "2023-09-11T11:34:56Z"
}
An invalid request.
POST http://localhost:8080/products HTTP/1.1
content-type: application/json
{
"name": "",
"category": "Equipment",
"description": "four",
"price": 345.99,
"stock": 30,
"dimensions": {},
"supplier": {
"name": "",
"contact": "123 Display Lane, San Francisco, CA",
"address": "sales@visiontech.com",
},
"tags": null,
"image": "image-here",
"manufacturedAt": "2019-09-11T11:34:56Z"
}
- Lets register a
PUT
endpoint for/products/:id
in themain
function. Here is the updatedmain
function:
func main() {
router := gin.Default()
router.GET("/products",
append(productQueriesValidators(), getProducts)...,
)
router.GET("/products/:id",
append(productParamIdValidators(), getProduct)...,
)
router.DELETE("/products/:id",
append(productParamIdValidators(), deleteProduct)...,
)
router.POST("/products",
append(productBodyValidators(), postProduct)...,
)
router.PUT("/products/:id",
append(append(productParamIdValidators(), productBodyValidators()...), putProducts)...,
)
router.Run(":8080")
}
We will re-use productParamIdValidators
and productBodyValidators
functions.
-
putProduct
: This is the final request handler middleware for thePOST
/products
route. It performs the following steps:- Validates the request parameters and body: Uses
gv.ValidationResult
to check for validation errors in both the route parameters and the request body. - Retrieves matched data using
gv.GetMatchedData
. - Finds the product: Matches the provided id with the existing products.
- Updates the product: If the product is found, it updates its details and sets a new UpdatedAt timestamp.
- It returns HTTP 200 with the updated product details if successful.
- It returns HTTP 404 if the product is not found.
- It returns HTTP 422 for validation errors.
- Validates the request parameters and body: Uses
func putProducts(c *gin.Context) {
result, err := gv.ValidationResult(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
if len(result) != 0 {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"message": "The server encountered validation error",
"data": nil,
"errors": result,
})
return
}
var newProduct Product
if err := c.BindJSON(&newProduct); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
data, err := gv.GetMatchedData(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": "The server encountered an unexpected error.",
"data": nil,
"errors": nil,
})
return
}
id, _ := data.Get(gv.ParamLocation, "id")
var EditedProduct *Product
var newProducts = make([]Product, len(products))
for ind, prod := range products {
if prod.ID == id {
newProduct.ID = id
newProduct.CreatedAt = prod.CreatedAt
newProduct.UpdatedAt = time.Now()
newProducts[ind] = newProduct
EditedProduct = &newProduct
} else {
newProducts[ind] = prod
}
}
if EditedProduct == nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"message": fmt.Sprintf("Product with id %s, not found", id),
"data": nil,
"errors": nil,
})
return
}
products = newProducts
c.AbortWithStatusJSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Product with id %s, has been edited", id),
"data": EditedProduct,
"errors": nil,
})
}
Run and test the code for PUT
/products
Quit the previous process and rerun:
go run main.go
A valid request.
PUT http://localhost:8080/products HTTP/1.1
content-type: application/json
{
"name": "Ultra 8k Monitor",
"category": "Electronics",
"description": "27-inch 8K Ultra HD monitor with HDR support and ultra-slim bezels.",
"price": 390.99,
"stock": 15,
"dimensions": {
"length": 16.0,
"width": 63.0,
"height": 0.5,
"weight": 4.5
},
"supplier": {
"name": "VisionTech Co.",
"contact": "sales@visiontech.com",
"address": "123 Display Lane, San Francisco, CA"
},
"tags": ["8K", "HDR", "electronics", "display"],
"image": "https://example.com/images/monitor1-edited.jpg",
"manufacturedAt": "2023-09-11T11:34:56Z"
}
An invalid request.
PUT http://localhost:8080/products HTTP/1.1
content-type: application/json
{
"name": "",
"category": "Equipment",
"description": "four",
"price": 345.99,
"stock": 30,
"dimensions": null,
"supplier": {
"name": "",
"contact": "123 Display Lane, San Francisco, CA",
"address": "sales@visiontech.com",
},
"tags": null,
"image": "image-here",
"manufacturedAt": "2019-09-11T11:34:56Z"
}
Final thoughts
Now you can see how straightforward it is to validate your HTTP requests in Gin
using both ginvalidator and validatorgo.
For more details on customizing error messages, check out this guide. To learn more about field syntax extraction, refer to this section.
If you prefer not to use the append method in the route registration handlers, you can break down the middleware validator handlers into smaller chunks and add them individually to the registration handler.
Thanks for reading, and see you next time!
Full Code
package main
import (
"fmt"
"net/http"
"regexp"
"time"
gv "github.com/bube054/ginvalidator"
vgo "github.com/bube054/validatorgo"
"github.com/gin-gonic/gin"
)
// Dimensions represents the physical dimensions of a product.
type Dimensions struct {
Length float64 `json:"length"` // Length of the product in centimeters
Width float64 `json:"width"` // Width of the product in centimeters
Height float64 `json:"height"` // Height of the product in centimeters
Weight float64 `json:"weight"` // Weight of the product in kilograms
}
// Supplier represents information about the supplier of a product.
type Supplier struct {
Name string `json:"name"` // Name of the supplier
Contact string `json:"contact"` // Contact details (e.g., email or phone number)
Address string `json:"address"` // Address of the supplier
}
// Product represents data about an inventory product.
type Product struct {
ID string `json:"id"` // Unique identifier for the product
Name string `json:"name"` // Name of the product
Category string `json:"category"` // Category the product belongs to
Description string `json:"description"` // Detailed description of the product
Price float64 `json:"price"` // Price of the product in decimal format
Stock int `json:"stock"` // Quantity of the product available in stock
Dimensions Dimensions `json:"dimensions"` // Physical dimensions of the product
Supplier Supplier `json:"supplier"` // Supplier details
Tags []string `json:"tags"` // Tags for searching and filtering
Image string `json:"image"` // URLs of product images
ManufacturedAt time.Time `json:"manufacturedAt"` // Timestamp when the product was manufactured
CreatedAt time.Time `json:"createdAt"` // Timestamp when the product was created
UpdatedAt time.Time `json:"updatedAt"` // Timestamp when the product was last updated
}
var products = []Product{
{
ID: "p1",
Name: "Wireless Mouse",
Category: "Electronics",
Description: "A high-precision wireless mouse with ergonomic design.",
Price: 29.99,
Stock: 150,
Dimensions: Dimensions{
Length: 11.5,
Width: 6.0,
Height: 3.5,
Weight: 0.2,
},
Supplier: Supplier{
Name: "Tech Supplies Inc.",
Contact: "support@techsupplies.com",
Address: "123 Tech Street, Silicon Valley, CA",
},
Tags: []string{"wireless", "mouse", "electronics", "accessories"},
Image: "https://example.com/images/mouse1.jpg",
ManufacturedAt: time.Now().AddDate(0, -30, 0),
CreatedAt: time.Now().AddDate(0, -1, 0),
UpdatedAt: time.Now().AddDate(0, -1, 0),
},
{
ID: "p2",
Name: "Gaming Keyboard",
Category: "Electronics",
Description: "Mechanical keyboard with customizable RGB lighting.",
Price: 79.99,
Stock: 75,
Dimensions: Dimensions{
Length: 45.0,
Width: 15.0,
Height: 3.8,
Weight: 1.1,
},
Supplier: Supplier{
Name: "Gaming Gear Ltd.",
Contact: "info@gaminggear.com",
Address: "456 Gaming Road, Austin, TX",
},
Tags: []string{"keyboard", "gaming", "RGB", "electronics"},
Image: "https://example.com/images/keyboard2.jpg",
ManufacturedAt: time.Now().AddDate(0, -20, 0),
CreatedAt: time.Now().AddDate(0, -2, 0),
UpdatedAt: time.Now().AddDate(0, -2, 0),
},
}
func main() {
router := gin.Default()
router.GET("/products",
append(productQueriesValidators(), getProducts)...,
)
router.GET("/products/:id",
append(productParamIdValidators(), getProduct)...,
)
router.DELETE("/products/:id",
append(productParamIdValidators(), deleteProduct)...,
)
router.POST("/products",
append(productBodyValidators(), postProduct)...,
)
router.PUT("/products/:id",
append(append(productParamIdValidators(), productBodyValidators()...), putProducts)...,
)
router.Run(":8080")
}
func productQueriesValidators() gin.HandlersChain {
return gin.HandlersChain{
gv.NewQuery("q", nil).Chain().Optional().Trim(" ").Not().Empty(nil).Validate(),
gv.NewQuery("order", nil).Chain().Optional().Trim(" ").In([]string{"asc", "desc"}).Validate(),
}
}
func productParamIdValidators() gin.HandlersChain {
return gin.HandlersChain{gv.NewParam("id", nil).Chain().Trim(" ").Alphanumeric(nil).Validate()}
}
func productBodyValidators() gin.HandlersChain {
var (
minDesc uint = 5
maxDesc uint = 100
minStock int = 0
validCategories []string = []string{"Electronics", "Apparels", "Groceries", "Home-Appliances"}
)
return gin.HandlersChain{
gv.NewBody("name", nil).Chain().Not().Empty(nil).Validate(),
gv.NewBody("category", nil).Chain().In(validCategories).Validate(),
gv.NewBody("description", nil).Chain().Length(&vgo.IsLengthOpts{Min: uint(minDesc), Max: &maxDesc}).Validate(),
gv.NewBody("price", nil).Chain().Decimal(&vgo.IsDecimalOpts{DecimalDigits: vgo.DecimalDigits{Min: 2}, ForceDecimal: false, Locale: "en-US"}).Validate(),
gv.NewBody("stock", nil).Chain().Int(&vgo.IsIntOpts{Min: &minStock}).Validate(),
gv.NewBody(`dimensions.length`, nil).Chain().Numeric(nil).Validate(),
gv.NewBody("dimensions.width", nil).Chain().Numeric(nil).Validate(),
gv.NewBody("dimensions.height", nil).Chain().Numeric(nil).Validate(),
gv.NewBody("dimensions.weight", nil).Chain().Numeric(nil).Validate(),
gv.NewBody("supplier.name", nil).Chain().Trim(" ").Not().Empty(nil).Validate(),
gv.NewBody("supplier.contact", nil).Chain().Email(nil).Validate(),
gv.NewBody("supplier.address", nil).Chain().Matches(regexp.MustCompile(`^\d+\s[\w\s]+,\s[\w\s]+,\s[A-Z]{2}$`)).Validate(),
gv.NewBody("tags", nil).Chain().Array(nil).Validate(),
gv.NewBody("image", nil).Chain().URL(nil).Validate(),
gv.NewBody("manufacturedAt", nil).Chain().Date(&vgo.IsDateOpts{Format: vgo.ISO8601ZuluLayout, StrictMode: true}).After(&vgo.IsAfterOpts{ComparisonDate: "2020-05-10T00:00:00Z"}).Validate(),
}
}
func getProducts(c *gin.Context) {
result, err := gv.ValidationResult(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
if len(result) != 0 {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"message": "The server encountered validation error",
"data": nil,
"errors": result,
})
return
}
c.AbortWithStatusJSON(http.StatusOK, gin.H{
"message": "The products found successfully",
"data": products,
"errors": nil,
})
}
func getProduct(c *gin.Context) {
result, err := gv.ValidationResult(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
if len(result) != 0 {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"message": "The server encountered validation error",
"data": nil,
"errors": result,
})
return
}
data, err := gv.GetMatchedData(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
id, _ := data.Get(gv.ParamLocation, "id")
var product *Product
for _, p := range products {
if p.ID == id {
product = &p
break
}
}
if product == nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"message": fmt.Sprintf("Product with id %s, not found", id),
"data": nil,
"errors": nil,
})
return
} else {
c.AbortWithStatusJSON(http.StatusOK, gin.H{
"message": "The product found successfully",
"data": product,
"errors": nil,
})
return
}
}
func deleteProduct(c *gin.Context) {
result, err := gv.ValidationResult(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
if len(result) != 0 {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"message": "The server encountered validation error",
"data": nil,
"errors": result,
})
return
}
data, err := gv.GetMatchedData(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": "The server encountered an unexpected error.",
"data": nil,
"errors": nil,
})
return
}
id, _ := data.Get(gv.ParamLocation, "id")
var deletedProduct *Product
var filteredProducts = []Product{}
for _, prod := range products {
if prod.ID == id {
deletedProduct = &prod
break
} else {
filteredProducts = append(filteredProducts, prod)
}
}
if deletedProduct == nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"message": fmt.Sprintf("Product with id %s, not found", id),
"data": nil,
"errors": nil,
})
return
}
products = filteredProducts
c.AbortWithStatusJSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Product with id %s, deleted", id),
"data": deletedProduct,
"errors": nil,
})
}
func postProduct(c *gin.Context) {
result, err := gv.ValidationResult(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
if len(result) != 0 {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"message": "The server encountered validation error",
"data": nil,
"errors": result,
})
return
}
var newProduct Product
if err := c.BindJSON(&newProduct); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
newProduct.ID = fmt.Sprintf("p%d", len(products)+1)
newProduct.CreatedAt = time.Now()
newProduct.UpdatedAt = newProduct.CreatedAt
products = append(products, newProduct)
c.AbortWithStatusJSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Product with id %s, has been created", newProduct.ID),
"data": newProduct,
"errors": nil,
})
}
func putProducts(c *gin.Context) {
result, err := gv.ValidationResult(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
if len(result) != 0 {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"message": "The server encountered validation error",
"data": nil,
"errors": result,
})
return
}
var newProduct Product
if err := c.BindJSON(&newProduct); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": fmt.Sprintf("The server encountered an unexpected error: %v", err),
"data": nil,
"errors": nil,
})
return
}
data, err := gv.GetMatchedData(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": "The server encountered an unexpected error.",
"data": nil,
"errors": nil,
})
return
}
id, _ := data.Get(gv.ParamLocation, "id")
var EditedProduct *Product
var newProducts = make([]Product, len(products))
for ind, prod := range products {
if prod.ID == id {
newProduct.ID = id
newProduct.CreatedAt = prod.CreatedAt
newProduct.UpdatedAt = time.Now()
newProducts[ind] = newProduct
EditedProduct = &newProduct
} else {
newProducts[ind] = prod
}
}
if EditedProduct == nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"message": fmt.Sprintf("Product with id %s, not found", id),
"data": nil,
"errors": nil,
})
return
}
products = newProducts
c.AbortWithStatusJSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Product with id %s, has been edited", id),
"data": EditedProduct,
"errors": nil,
})
}
Top comments (0)