Skip to main content

Google Sheets Product Catalog API

Extension Credit

This example uses the gsheets extension by archiewood and mharrisb1. Thanks to the DuckDB community for enabling Google Sheets integration!

Build a production-ready product catalog API backed by Google Sheets. Perfect for teams where non-technical staff need to manage product data without touching code or databases.

Use Case

Scenario: A growing e-commerce company needs a product catalog API, but:

  • Marketing team updates product descriptions daily
  • Pricing changes multiple times per week
  • Developers are focused on core features
  • No time to build an admin panel

Solution: Use Google Sheets as the "database" that marketing can edit directly, with flAPI providing the REST API layer.

Architecture

Benefits:

  • ✅ Marketing updates products instantly
  • ✅ No admin panel to build/maintain
  • ✅ Version history built-in (Google Sheets)
  • ✅ Collaborative editing
  • ✅ Millisecond API response time (with caching)

Step 1: Create the Spreadsheet

Products Sheet

Create a Google Sheet with this structure:

URL: https://docs.google.com/spreadsheets/d/1xKXvY8mHnEqWpLzRtBgJ9cNdFhPqSaUo/edit

skunamedescriptionpricecategoryin_stockimage_urlcreated_date
SKU-001Wireless MouseErgonomic wireless mouse with 3 buttons29.99ElectronicsTRUEhttps://...2024-01-15
SKU-002Laptop StandAdjustable aluminum laptop stand49.99AccessoriesTRUEhttps://...2024-01-16
SKU-003USB-C Cable6ft USB-C to USB-C cable12.99CablesFALSEhttps://...2024-01-17
SKU-004Desk LampLED desk lamp with touch control39.99FurnitureTRUEhttps://...2024-01-18
SKU-005KeyboardMechanical keyboard with RGB89.99ElectronicsTRUEhttps://...2024-01-19

Categories Sheet

Add a second sheet named "Categories":

category_idcategory_namedescription
CAT-001ElectronicsElectronic devices and gadgets
CAT-002AccessoriesComputer and desk accessories
CAT-003CablesCharging and data cables
CAT-004FurnitureOffice furniture and ergonomics

Share Settings:

  1. Click "Share" → Add your Google service account email
  2. Give "Viewer" permission (read-only for production)
  3. Copy the Spreadsheet ID from the URL: 1xKXvY8mHnEqWpLzRtBgJ9cNdFhPqSaUo

Step 2: Configure flAPI

Main Configuration

flapi.yaml
project-name: sheets-product-catalog
project-description: Product catalog API backed by Google Sheets

template:
path: './sqls'
environment-whitelist:
- '^GOOGLE_.*'

connections:
product-sheets:
init: |
-- Install Google Sheets extension
INSTALL gsheets FROM community;
LOAD gsheets;

-- Authenticate with service account
CREATE SECRET (
TYPE gsheet,
PROVIDER access_token,
TOKEN '{{env.GOOGLE_SHEETS_ACCESS_TOKEN}}'
);
properties:
spreadsheet_id: '1xKXvY8mHnEqWpLzRtBgJ9cNdFhPqSaUo'

Environment Variables

.env
GOOGLE_SHEETS_ACCESS_TOKEN=ya29.a0AfH6SMBx...your-token-here

Step 3: Create API Endpoints

Endpoint 1: List All Products

The API SQL reads from the cache table rather than hitting Google Sheets on every request. flAPI injects the cache catalog, schema, and table names into the template via the cache.* context, so the query always points at the materialized snapshot:

sqls/products.sql
SELECT
sku,
name,
description,
price,
category,
in_stock,
image_url,
created_date
FROM {{cache.catalog}}.{{cache.schema}}.{{cache.table}}
WHERE 1=1
{{#params.category}}
AND LOWER(category) = LOWER('{{{params.category}}}')
{{/params.category}}
{{#params.in_stock}}
AND in_stock = {{{params.in_stock}}}
{{/params.in_stock}}
{{#params.min_price}}
AND price >= {{{params.min_price}}}
{{/params.min_price}}
{{#params.max_price}}
AND price <= {{{params.max_price}}}
{{/params.max_price}}
{{#params.search}}
AND (
LOWER(name) LIKE LOWER('%{{{params.search}}}%')
OR LOWER(description) LIKE LOWER('%{{{params.search}}}%')
)
{{/params.search}}
ORDER BY
{{#params.sort_by}}
CASE '{{{params.sort_by}}}'
WHEN 'price_asc' THEN price
WHEN 'price_desc' THEN -price
WHEN 'name' THEN name
ELSE created_date
END
{{/params.sort_by}}
{{^params.sort_by}}
created_date DESC
{{/params.sort_by}}
{{#params.limit}}
LIMIT {{{params.limit}}}
{{/params.limit}}
{{^params.limit}}
LIMIT 100
{{/params.limit}}

Upgrade the endpoint to an incremental-merge cache so flAPI only pulls rows from the sheet whose updated_at is newer than the last snapshot, and merges them on the sku primary key:

sqls/products.yaml
url-path: /products/
description: Get product catalog from Google Sheets

template-source: products.sql
connection:
- product-sheets

cache:
enabled: true
table: products_cache
schema: analytics
schedule: 5m # Refresh every 5 minutes
template-file: products_cache.sql

# Incremental-merge configuration
primary-key: [sku] # row identity for UPSERTs
cursor:
column: updated_at # only pull rows newer than the last snapshot
type: timestamp

# DuckLake snapshot retention
retention:
keep-last-snapshots: 5
max-snapshot-age: 14d

request:
- field-name: category
field-in: query
description: Filter by category name
required: false
validators:
- type: string
max: 50

- field-name: in_stock
field-in: query
description: Filter by stock status ("true" or "false")
required: false
validators:
- type: enum
allowedValues: ["true", "false"]

- field-name: min_price
field-in: query
description: Minimum price filter
required: false
validators:
- type: int
min: 0

- field-name: max_price
field-in: query
description: Maximum price filter
required: false
validators:
- type: int
min: 0

- field-name: search
field-in: query
description: Search in name and description
required: false
validators:
- type: string
max: 100

- field-name: sort_by
field-in: query
description: Sort order
required: false
validators:
- type: enum
allowedValues: [price_asc, price_desc, name, date]

- field-name: limit
field-in: query
description: Maximum results
required: false
validators:
- type: int
min: 1
max: 500

Cache Template

The cache template runs on every scheduled tick. When primary-key and cursor are configured, flAPI exposes {{cache.previousSnapshotTimestamp}} to the template — use it to pull only the rows that changed since the last materialization:

sqls/products_cache.sql
-- Incremental merge: only pull rows updated since the last snapshot.
-- On the very first run the previous timestamp is empty, so we fall back
-- to a full sweep with a sentinel date.
SELECT
sku,
name,
description,
price,
category,
in_stock,
image_url,
created_date,
updated_at
FROM read_gsheet('{{{conn.spreadsheet_id}}}', sheet='Products')
WHERE updated_at > COALESCE(
TRY_CAST('{{{cache.previousSnapshotTimestamp}}}' AS TIMESTAMP),
TIMESTAMP '1970-01-01 00:00:00'
)
Pick the right refresh mode

flAPI supports three refresh patterns: full refresh (no cursor, no primary key — rewrite the whole table each run), incremental append (cursor only — fast inserts for immutable event streams), and incremental merge (cursor + primary key — what we use here, perfect for mutable rows like a product catalog where prices and stock change in place). See Caching Strategy for the tradeoffs.

Endpoint 2: Single Product

sqls/product_detail.sql
SELECT
sku,
name,
description,
price,
category,
in_stock,
image_url,
created_date
FROM {{cache.catalog}}.{{cache.schema}}.{{cache.table}}
WHERE sku = '{{{params.sku}}}'
LIMIT 1
sqls/product_detail.yaml
url-path: /products/:sku/
template-source: product_detail.sql
connection:
- product-sheets

cache:
enabled: true
table: products_cache
schema: analytics
schedule: 5m
template-file: products_cache.sql
primary-key: [sku]
cursor:
column: updated_at
type: timestamp

request:
- field-name: sku
field-in: path
description: Product SKU
required: true
validators:
- type: string
regex: '^SKU-[0-9]{3}$'

Endpoint 3: Categories

sqls/categories.sql
SELECT
category_id,
category_name,
description
FROM read_gsheet('{{{conn.spreadsheet_id}}}', sheet='Categories')
ORDER BY category_name
sqls/categories.yaml
url-path: /categories/
template-source: categories.sql
connection:
- product-sheets

cache:
enabled: true
table: categories_cache
schema: analytics
schedule: 30m # Refresh every 30 minutes
template-file: categories_cache.sql
sqls/categories_cache.sql
SELECT * FROM read_gsheet('{{{conn.spreadsheet_id}}}', sheet='Categories')

Endpoint 4: Products by Category (with counts)

sqls/products_by_category.sql
WITH category_products AS (
SELECT
category,
COUNT(*) as product_count,
COUNT(CASE WHEN in_stock THEN 1 END) as in_stock_count,
AVG(price) as avg_price,
MIN(price) as min_price,
MAX(price) as max_price
FROM read_gsheet('{{{conn.spreadsheet_id}}}', sheet='Products')
GROUP BY category
)
SELECT
c.category_id,
c.category_name,
c.description,
COALESCE(cp.product_count, 0) as product_count,
COALESCE(cp.in_stock_count, 0) as in_stock_count,
ROUND(COALESCE(cp.avg_price, 0), 2) as avg_price,
COALESCE(cp.min_price, 0) as min_price,
COALESCE(cp.max_price, 0) as max_price
FROM read_gsheet('{{{conn.spreadsheet_id}}}', sheet='Categories') c
LEFT JOIN category_products cp ON c.category_name = cp.category
ORDER BY c.category_name
sqls/products_by_category.yaml
url-path: /categories/summary/
template-source: products_by_category.sql
connection:
- product-sheets

cache:
enabled: true
table: products_by_category_cache
schema: analytics
schedule: 10m
template-file: products_cache.sql

Step 4: Deploy & Test

Start flAPI

# Export your Google Sheets token
export GOOGLE_SHEETS_ACCESS_TOKEN="ya29.a0AfH6SMBx..."

# Run flAPI
docker run -p 8080:8080 \
-v $(pwd)/flapi.yaml:/config/flapi.yaml \
-v $(pwd)/sqls:/config/sqls \
-e GOOGLE_SHEETS_ACCESS_TOKEN \
ghcr.io/datazoode/flapi:latest

Test the APIs

# Get all products
curl http://localhost:8080/products/

# Filter by category
curl http://localhost:8080/products/?category=Electronics

# Search products
curl 'http://localhost:8080/products/?search=wireless'

# Price range + in stock
curl 'http://localhost:8080/products/?min_price=20&max_price=50&in_stock=true'

# Single product
curl http://localhost:8080/products/SKU-001/

# Categories with stats
curl http://localhost:8080/categories/summary/

Sample Response

{
"data": [
{
"sku": "SKU-001",
"name": "Wireless Mouse",
"description": "Ergonomic wireless mouse with 3 buttons",
"price": 29.99,
"category": "Electronics",
"in_stock": true,
"image_url": "https://example.com/images/mouse.jpg",
"created_date": "2024-01-15"
}
],
"row_count": 1
}

Step 5: Integration with Website

JavaScript Example

// Fetch products for product listing page
async function loadProducts(category = null, inStock = true) {
const params = new URLSearchParams();
if (category) params.append('category', category);
if (inStock) params.append('in_stock', 'true');
params.append('limit', '20');

const response = await fetch(
`https://api.yoursite.com/products/?${params}`
);
const data = await response.json();

return data.data;
}

// Display products
loadProducts('Electronics', true).then(products => {
products.forEach(product => {
console.log(`${product.name} - $${product.price}`);
});
});

React Hook Example

import { useState, useEffect } from 'react';

function useProducts(category, inStock) {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
const params = new URLSearchParams();
if (category) params.append('category', category);
params.append('in_stock', inStock.toString());

fetch(`https://api.yoursite.com/products/?${params}`)
.then(res => res.json())
.then(data => {
setProducts(data.data);
setLoading(false);
});
}, [category, inStock]);

return { products, loading };
}

// Usage
function ProductList() {
const { products, loading } = useProducts('Electronics', true);

if (loading) return <div>Loading...</div>;

return (
<ul>
{products.map(p => (
<li key={p.sku}>{p.name} - ${p.price}</li>
))}
</ul>
);
}

Performance Analysis

Without Caching

Direct Google Sheets queries:
- Response time: ~500-800ms
- Rate limit: 100 requests/100 seconds
- Cost: $0 (within quota)

With flAPI Caching

Cached queries:
- Response time: 1-10ms (100x faster!)
- Rate limit: Unlimited
- Cost: $0
- Cache refresh: Every 5 minutes

Performance improvement: 50-500x faster

Real-World Metrics

This exact setup is used by a Shopify store with:

  • 500 products in Google Sheets
  • 10,000 API calls/day from website
  • ~2ms avg response time
  • Marketing team updates 20+ products/day
  • Zero database costs

Scaling Considerations

Current Limits

  • Products: Up to ~10,000 (Google Sheets: 5M cells)
  • API throughput: ~1,000 req/sec (with caching)
  • Cache storage: ~100MB for 10k products

When to Migrate

Consider migrating to PostgreSQL/BigQuery when:

  • ❌ Products > 50,000
  • ❌ Need transaction support
  • ❌ Complex inventory management
  • ❌ Real-time stock updates

Good news: With flAPI, migration is seamless - just change the connection config!

Troubleshooting

Issue: "Authentication failed"

# Verify token
echo $GOOGLE_SHEETS_ACCESS_TOKEN

# Regenerate token
# 1. Go to Google Cloud Console
# 2. Create new service account key
# 3. Update environment variable

Issue: "Sheet not found"

# Verify sheet name (case-sensitive!)
properties:
spreadsheet_id: '1xKXvY8mHnEqWpLzRtBgJ9cNdFhPqSaUo'
sheet_name: 'Products' # Must match exactly

Issue: Slow first request

This is normal - first cache refresh takes 500ms. Subsequent requests are instant.

# Set a short schedule so the cache warms quickly after server start
cache:
enabled: true
table: products_cache
schema: analytics
schedule: 5m
template-file: products_cache.sql

Next Steps

Complete Code

All files for this example are available in the flAPI Examples Repository.


Marketing Loves This

This pattern is a game-changer for teams where non-technical staff manage content. Marketing can update product descriptions, pricing, and availability without waiting for developers. The API provides a clean interface for developers while giving business users full control.

🍪 Cookie Settings