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:

mkdir 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. In this file, we’ll write the model:

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": "Item deleted successfully"})
      return
    }
  }

  c.JSON(http.StatusNotFound, gin.H{"message": "Item not found"})
}

Conclusion

Now that all the CRUD operations are in place, it's time to give our grocery application a spin.

You can start the application by running the following:

go run main.go

This command compiles and starts your application. If everything is configured correctly, you will see a message stating that your server is running:

[GIN-debug] Listening and serving HTTP on :8080

You can now use your browser, CURL, or a tool like postman to test out the app!

In this article, we've walked through how to use Go and Gin to build a performant CRUD API that keeps track of your grocery inventory! While limited in scope, this represents solving a very real problem. If you're interested in scaling the application beyond this initial scope, consider adding a database, expanding on error validation, and even writing unit tests! Have fun exploring Go and Gin!

Get the Honeybadger newsletter

Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo
    Jeffery Morhous

    Jeff is a Software Engineer working in healtcare technology using Ruby on Rails, React, and plenty more tools. He loves making things that make life more interesting and learning as much he can on the way. In his spare time, he loves to play guitar, hike, and tinker with cars.

    More articles by Jeffery Morhous
    An advertisement for Honeybadger that reads 'Turn your logs into events.'

    "Splunk-like querying without having to sell my kidneys? nice"

    That’s a direct quote from someone who just saw Honeybadger Insights. It’s a bit like Papertrail or DataDog—but with just the good parts and a reasonable price tag.

    Best of all, Insights logging is available on our free tier as part of a comprehensive monitoring suite including error tracking, uptime monitoring, status pages, and more.

    Start logging for FREE
    Simple 5-minute setup — No credit card required