Have you ever found yourself rummaging through the fridge, trying to figure out what groceries you have and what you need to buy? Okay, maybe this isn’t a super serious problem, but what better way to manage this chore than by building a web API? In this guide, we’ll craft a grocery management API with Go and Gin. If you’re new to API lingo, CRUD might sound a bit … well, crude. However, it’s a fundamental concept! CRUD stands for create, read, update, and delete. These represent the basic operations you’ll need for persistent storage. Much of the internet can be modeled this way. Gin is a blazing-fast web framework for Go. In addition to its performance, Gin is known for ease of integration with middleware like logging and error handling. Beyond that, it’s concise and intuitive, so it’s approachable as a newcomer. By the end of this article, you’ll have a fully-functional API capable of managing your grocery list, all built with Go and Gin.
Setting up your Go development environment
Let’s get the ball rolling by ensuring our local machine is dressed up for the Go party. 🎉 If you’re used to programming in other languages, setting up Go will feel familiar.
Installing Go
To begin, head over to the official Go downloads page and grab the installer for your operating system. Run it, and within a few clicks, you’ll have Go installed. You can verify Go is installed by checking the version. In your command line, run the following:
go version
On my machine, this returns the following:
go version go1.20.4 darwin/arm64
Installing Gin
We’ll use the Gin web framework for this project, and you can install it with Go’s built-in package manager. First, create a new directory to house the API we’ll create:
go-gin-api
You can substitute go-gin-api with whatever you’d like to name the project. Next, cd into the directory and initialize a new Go project:
go mod init go-gin-api
Again, you can substitute go-gin-api here for your chosen name. Lastly, install Gin:
go get -u github.com/gin-gonic/gin
Designing the grocery inventory model
Before we jump into all the routing and handling magic, let’s lay down the foundation. The core of our grocery management API lies in how we represent a grocery item. Like all object-oriented programming, we’ll represent a concept with an object, or model. It’s a bit more involved than scribbling items on a piece of paper, but this is the digital age! Our “Grocery Item” model will have three attributes: name, count, and category. For the sake of this project, we’ll store our objects in an in-memory array instead of a database. If you’re looking to scale this or have better persistence, then consider replacing it with a SQL database. First, create a directory named models in the root of your project. In this directory, create a file titled groceryItem.go.
package models
type GroceryItem struct {
ID int `json:"id"`
Name string `json:"name"`
Count int `json:"count"`
Category string `json:"category"`
}
Next, create a directory called data in the root of your repo. In this directory, create a file called store.go. In this file, we’ll set up our array that stores GroceryItems.
package data
import "go-gin-api/models"
// Our in-memory 'database' of grocery items.
var GroceryDB = []models.GroceryItem{}
// A simple counter for our IDs, to simulate auto-increment since we're not using a real DB.
var IDCounter = 1
With our model and data store in place, we can move on to accepting incoming web requests!
Building the CRUD endpoints
Before writing the code to handle data, create a file named main.go in the root of your project that will serve as an entry point for incoming requests. Here’s what we’ll start with:
package main
import (
"go-gin-api/handlers"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// Register the route to our handler function
r.POST("/groceries", handlers.AddGroceryItem)
r.Run() // default listens on :8080
}
This file imports our handlers directory, which we’ll write next, as well as the gin framework. In the main function, we create a route to handle POST requests to the /groceries endpoint and set the handler to a file we’ll create next.
Create
Let’s start by ensuring our digital pantry can receive new stock (or, in layman’s terms, add items to the data store). We’ll use a POST request to /groceries to symbolize the addition of a new item. It’s intuitive and in line with common REST practices. We also want to ensure that the data coming in is not only readable but also valid. This includes making sure essential fields are filled and that the data types are correct. Once we’ve done that, we’ll construct a new grocery item and append it to our in-memory database. Create a new directory in the root of your project named handlers, and in it, create a file called groceryHandlers.go. Here’s what we’ll start with:
package handlers
import (
"net/http"
"strconv"
"go-gin-api/data"
"go-gin-api/models"
"github.com/gin-gonic/gin"
)
func AddGroceryItem(c *gin.Context) {
var newItem models.GroceryItem
if err := c.ShouldBindJSON(&newItem); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Assign a new ID and append to the database
newItem.ID = data.IDCounter
data.GroceryDB = append(data.GroceryDB, newItem)
data.IDCounter++
c.JSON(http.StatusCreated, newItem)
}
With this code, our API can now accept new grocery items and give them a unique ID.
Read
Storing information about grocery items isn’t particularly useful if we can’t retrieve it. Next, we’ll create the endpoints for reading grocery items from the system. First, we’ll define an endpoint to fetch the details of a specific grocery item, and then we’ll define an endpoint to list all grocery items. To get a specific item, we’ll set up a GET request to /groceries/:id, where :id is a dynamic parameter representing the unique ID of the grocery item. In your main.go file, you will insert additional routes for these two endpoints. Append the following code in this file below your first route:
// New GET routes
r.GET("/groceries", handlers.ListGroceryItems)
r.GET("/groceries/:id", handlers.GetGroceryItem)
Next, we’ll add our handler code to handlers/groceryHandlers.go. In this file, add the following functions:
// Fetch a specific grocery item by ID
func GetGroceryItem(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
for _, item := range data.GroceryDB {
if item.ID == id {
c.JSON(http.StatusOK, item)
return
}
}
c.JSON(http.StatusNotFound, gin.H{"message": "Item not found"})
}
// List all grocery items
func ListGroceryItems(c *gin.Context) {
c.JSON(http.StatusOK, data.GroceryDB)
}
With these routes in place, we can now fetch individual items by their ID or get a comprehensive list of all items.
Update
When it comes to updating, we need to ensure we’re pinpointing the right item and making the intended modifications. A PUT request to /groceries/:id follows best practices. The :id parameter will identify the item we intend to update. Our handler will locate the item by its ID in our in-memory datastore, and then update the relevant fields based on the provided input. In main.go, add the new route:
r.PUT("/groceries/:id", handlers.UpdateGroceryItem)
Next, we’ll add the handler for this endpoint to handlers/groceryHandlers.go. Add the following function to the file:
// Update a specific grocery item by ID
func UpdateGroceryItem(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var updatedItem models.GroceryItem
if err := c.ShouldBindJSON(&updatedItem); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for index, item := range data.GroceryDB {
if item.ID == id {
data.GroceryDB[index].Name = updatedItem.Name
data.GroceryDB[index].Count = updatedItem.Count
data.GroceryDB[index].Category = updatedItem.Category
c.JSON(http.StatusOK, data.GroceryDB[index])
return
}
}
c.JSON(http.StatusNotFound, gin.H{"message": "Item not found"})
}
With this PUT endpoint, we can now modify an item’s name, count, or category based on its ID. Remember, while our in-memory database makes the process straightforward, using a real database will likely introduce additional complexities.
Delete
Building an endpoint to allow deletion of a grocery item is pretty straightforward. For deletion, a DELETE request to /groceries/:id follows best practices. As before, the :id parameter will point us to the item that’s up for removal. The handler will simply identify the item by its ID in our in-memory database and remove it. First, add the new route. In main.go, add the following to the list of routes:
// New DELETE route
r.DELETE("/groceries/:id", handlers.DeleteGroceryItem)
Next, we’ll add the new handler. In handlers/groceryHandlers.go, add the following new function:
// Delete a specific grocery item by ID
func DeleteGroceryItem(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
for index, item := range data.GroceryDB {
if item.ID == id {
// Remove item from our "database"
data.GroceryDB = append(data.GroceryDB[:index], data.GroceryDB[index+1:]...)
c.JSON(http.StatusOK, gin.H{"message": ...