💾 Go Sheet DB: I Built a REST API in Go Using Google Sheets as a Database, Then Dockerized It Like I Hate Myself
Cyber

Cyber @cyberdev_

Joined:
Apr 16, 2024

💾 Go Sheet DB: I Built a REST API in Go Using Google Sheets as a Database, Then Dockerized It Like I Hate Myself

Publish Date: Jun 23
1 0

Welcome to Go Sheet DB (API) ™ - the only REST API held together by sheer willpower, a Google Sheet, and my irrational desire to do something in Go.

What started as "I should do something in Go" became a full-blown REST API running on Google Sheets as the backend database. No Postgres. No SQLite. Just... Sheets. The kind with rows and columns and sadness.

👶 Why Google Sheets?

Because I didn't want to install a database. And I love bad ideas that technically work.

✅ Free
✅ Serverless (in the worst way possible)
✅ Editable by non-devs (like your boss)
✅ Feels illegal - but isn't

🧙 Step 1: Set Up Google Sheets API Like It's 2007

  1. Go to the Google Cloud Console
  2. Create a new project
  3. Go to APIs & Services > Library, search for "Google Sheets API", and enable it
  4. Then go to APIs & Services > Credentials > Create Credentials > Service Account
  5. Once your Service account is created, click it email, later go to "Keys" > Add key > Create new key and download config.json
  6. Download the JSON key (put this in config.json)
  7. Share your Google Sheet with the service account email (something like my-api-thingy@your-project.iam.gserviceaccount.com)
  8. Breathe deeply. You're now legally a spreadsheet engineer.

📄 Step 2: Create Your "Database"

Create a new Google Sheet. Call it something like users_db.

In the first sheet/tab, name the columns:

ID | Name | Email | CreatedAt
Enter fullscreen mode Exit fullscreen mode

That's your schema. You're welcome.

💻 Step 3: Go Code That Shouldn't Exist But Does

Let's write some Go that connects to this spreadsheet and makes it behave like a proper database.

Install Dependencies

go mod init gosheetdb 
go get google.golang.org/api/sheets/v4 
go get golang.org/x/oauth2/google 
go get github.com/gin-gonic/gin
go install github.com/swaggo/swag/cmd/swag@latest
go get github.com/swaggo/gin-swagger
go get github.com/swaggo/files

Enter fullscreen mode Exit fullscreen mode

main.go (All in One Like a Champion)

// @title           Go Sheet DB API (aka Go Sh**t DB API)
// @version         1.0
// @description     Local API that stores users in Google Sheets
// @host            localhost:8081
// @BasePath        /

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/gin-gonic/gin"
    _ "gosheetdb/docs" // Swagger docs generated by swag init
    "github.com/swaggo/files"
    "github.com/swaggo/gin-swagger"

    "golang.org/x/oauth2/google"
    "google.golang.org/api/option"
    "google.golang.org/api/sheets/v4"
)

var (
    srv     *sheets.Service
    sheetID = "1n3Vt0_zLdMspL5VEzbD7SVWgK9186k-GRLv5xcjrFDc"
)

// User represents a user stored in Google Sheets
type User struct {
    ID        string `json:"id" example:"1234567890"`
    Name      string `json:"name" example:"Cyber"`
    Email     string `json:"email" example:"cyber@spreadsheet.go"`
    CreatedAt string `json:"createdAt" example:"2025-06-22T15:04:05Z"`
}

type ErrorResponse struct {
    Error string `json:"error" example:"some error message"`
}

func initSheets() {
    ctx := context.Background()
    b, err := os.ReadFile("config.json")
    if err != nil {
        log.Fatalf("Failed to read config.json: %v", err)
    }

    config, err := google.JWTConfigFromJSON(b, sheets.SpreadsheetsScope)
    if err != nil {
        log.Fatalf("Failed to parse config: %v", err)
    }

    client := config.Client(ctx)
    srv, err = sheets.NewService(ctx, option.WithHTTPClient(client))
    if err != nil {
        log.Fatalf("Failed to create Sheets client: %v", err)
    }
}

// getUsers godoc
// @Summary      Get all users
// @Description  Retrieve all users from Google Sheets
// @Tags         users
// @Produce      json
// @Success      200  {array}   User
// @Failure      500  {object}  ErrorResponse
// @Router       /users [get]
func getUsers(c *gin.Context) {
    resp, err := srv.Spreadsheets.Values.Get(sheetID, "Users!A2:D").Do()
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    var users []User
    for _, row := range resp.Values {
    if len(row) < 4 {
            continue
        }
        user := User{
            ID:        fmt.Sprintf("%v", row[0]),
            Name:      fmt.Sprintf("%v", row[1]),
            Email:     fmt.Sprintf("%v", row[2]),
            CreatedAt: fmt.Sprintf("%v", row[3]),
        }
        users = append(users, user)
    }

    c.JSON(http.StatusOK, users)
}

// addUser godoc
// @Summary      Add a new user
// @Description  Append a new user to Google Sheets
// @Tags         users
// @Accept       json
// @Produce      json
// @Param        user  body      User  true  "User to add"
// @Success      201   {object}  User
// @Failure      400   {object}  ErrorResponse
// @Failure      500   {object}  ErrorResponse
// @Router       /users [post]
func addUser(c *gin.Context) {
    var u User
    if err := c.BindJSON(&u); 
    err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Bad JSON"})
        return
    }

    u.ID = fmt.Sprintf("%d", time.Now().UnixNano())
    u.CreatedAt = time.Now().Format(time.RFC3339)

    v := &sheets.ValueRange{
        Values: [][]interface{}{
            {u.ID, u.Name, u.Email, u.CreatedAt},
        },
    }

    _, err := srv.Spreadsheets.Values.Append(sheetID, "Users!A:D", v).ValueInputOption("RAW").Do()
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusCreated, u)
}

func main() {
    initSheets()

    r := gin.Default()

    // Swagger UI at /swagger/index.html
    r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

    // API routes
    r.GET("/users", getUsers)
    r.POST("/users", addUser)

    port := os.Getenv("PORT")
    if port == "" {
        port = "8081"
    }

    r.Run(":" + port)
}

Enter fullscreen mode Exit fullscreen mode
echo 'export PATH="$HOME/go/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
swag init
Enter fullscreen mode Exit fullscreen mode

📦 How to Run It

  1. Add your real Google Sheet ID where it says sheetID = "..."
  2. Put your config.json in the root directory
  3. Run it:
go run main.go

Enter fullscreen mode Exit fullscreen mode
  1. Test it:
curl -X POST http://localhost:8081/users -H "Content-Type: application/json" -d '{"name": "Cyber", "email": "cyber@spreadsheet.go"}'

curl http://localhost:8081/users
Enter fullscreen mode Exit fullscreen mode

🎉 You just built an API powered by a spreadsheet.

📦 Step 4. Dockerfile From the Abyss

FROM golang:1.23 
WORKDIR /app 
COPY go.mod go.sum ./ 
RUN go mod download 
COPY . . 
RUN go build -o gosheetdb . 
CMD ["./gosheetdb"]
Enter fullscreen mode Exit fullscreen mode

Fast. Irresponsible. Beautiful.

🧼 .dockerignore - Hide the Evidence

config.json .git *.log
Enter fullscreen mode Exit fullscreen mode

Don't ship your secrets or your entire git history.
You're building a meme, not a monolith.

🧪 Run It Locally (For Science)

docker build -t gosheetdb .

docker run -p 8081:8081 -v $(pwd)/config.json:/app/config.json gosheetdb
Enter fullscreen mode Exit fullscreen mode

And just like that, your spreadsheet is now a cloud-native database.
Somewhere, a DBA just felt a cold chill.

🛠️ Real-Life Uses for Go Sheet DB (Weirdly Viable)

Despite being built on pure tech blasphemy, this API can work for small, chaotic, and oddly practical use cases:

  1. 💬 Event RSVP Tracking (Without a Real Backend)
  2. 📋 Internal Tools for Non-Devs
  3. 🧪 Prototyping APIs Before You Commit to a Real DB
  4. 🎓 Teaching & Workshops
  5. 📈 Collecting Feedback, Bug Reports, or Feature Votes

🤔 Final Thoughts

Sure, it was a wacky little experiment, but hey, now you know what a REST API in Go might look like, how to sweet-talk the Google Sheets API, and how to shove the whole thing into a Docker container like a tech-savvy magician.

  • I regret everything
  • I also regret nothing
  • It works
  • And that’s the real problem

Want to witness the beautiful chaos for yourself? The full code lives here (handle with care): https://github.com/ppaczk0wsk1/go-sheet-api

Comments 0 total

    Add comment