This tutorial introduces the basics of writing a RESTful web service API with . Which uses my two open source libraries: .
.
.
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 and . 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
CODEpackage 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: "[email protected]",
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: "[email protected]",
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,
})
}
↗ Original-Artikel auf dev.to lesenVollständiger Original-BerichtAusführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
SOCIAL SHARE CARD GENERATOR