Use Google Sheets as a Data Source in Django REST Framework
Fetch, filter, and serve Google Sheets data from a Django API - no database required for read-heavy use cases.
When this pattern makes sense
There's a category of projects where reaching for PostgreSQL is instinctive but probably overkill: internal dashboards, product catalogs managed by non-technical editors, MVP backends where the data model changes every week, and reporting tools where the source of truth is already a spreadsheet that five people actively maintain.
Shipping a Django API that reads directly from Google Sheets means your data editors stay in a tool they know, and your API consumers get a clean REST interface. When the data is read-heavy - many reads, infrequent writes, no transactional requirements - this architecture is genuinely pragmatic, not a hack.
GKit SheetsAPI sits in the middle: it authenticates your requests, handles the Google API plumbing, and returns clean JSON. You write a thin Django service class and a ViewSet. That's the whole backend.
Sheet setup
Create a Google Sheet with these columns in row 1:
| sku | name | category | price | stock | active |
|---|---|---|---|---|---|
| WH-001 | Wireless Headphones | Electronics | 89.99 | 142 | true |
| KB-002 | Mechanical Keyboard | Electronics | 129.00 | 38 | true |
| DSK-003 | Bamboo Desk Organizer | Office | 34.50 | 0 | false |
Name the sheet tab inventory. SheetsAPI uses the tab name as the sheetName path segment, so whatever you name the tab is what you'll use in the URL.
Connect the spreadsheet to GKit, copy your user key from the dashboard, and generate a bearer token from the API Keys page. Keep both handy - they go into your Django settings.
The service class
Install requests if you don't already have it:
pip install requestsCreate inventory/services.py:
import requests
from django.conf import settings
class SheetsAPIClient:
BASE_URL = "https://api.gkit.io/api/spreadsheets"
def __init__(self):
self.user_key = settings.GKIT_USER_KEY
self.api_key = settings.GKIT_API_KEY
self.sheet_name = settings.GKIT_SHEET_NAME # e.g. "inventory"
self.session = requests.Session()
self.session.headers.update(
{"Authorization": f"Bearer {self.api_key}"}
)
def _url(self):
return f"{self.BASE_URL}/{self.user_key}/{self.sheet_name}"
def list(self, search=None, sort=None, limit=50, offset=0, fields=None):
params = {"limit": limit, "offset": offset}
if search:
params["search"] = search # e.g. "category:Electronics"
if sort:
params["sort"] = sort # e.g. "price" or "-price"
if fields:
params["fields"] = fields # comma-separated column names
response = self.session.get(self._url(), params=params, timeout=8)
response.raise_for_status()
return response.json() # { data: [...], meta: { total, limit, offset } }Add the settings to settings.py:
GKIT_USER_KEY = "your_user_key_here"
GKIT_API_KEY = "sk_live_..."
GKIT_SHEET_NAME = "inventory"In production, load these from environment variables instead of hardcoding them. django-environ or a plain os.getenv both work fine.
The DRF ViewSet
Install Django REST Framework if needed:
pip install djangorestframeworkCreate inventory/views.py:
from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
from rest_framework import status
from .services import SheetsAPIClient
class InventoryViewSet(ViewSet):
def list(self, request):
client = SheetsAPIClient()
# Pull supported query params from the request
search = request.query_params.get("search")
sort = request.query_params.get("sort")
fields = request.query_params.get("fields")
try:
limit = int(request.query_params.get("limit", 50))
offset = int(request.query_params.get("offset", 0))
except ValueError:
return Response(
{"detail": "limit and offset must be integers."},
status=status.HTTP_400_BAD_REQUEST,
)
try:
result = client.list(
search=search,
sort=sort,
limit=limit,
offset=offset,
fields=fields,
)
except Exception as exc:
return Response(
{"detail": str(exc)},
status=status.HTTP_502_BAD_GATEWAY,
)
return Response(result)Wire it up in urls.py:
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from inventory.views import InventoryViewSet
router = DefaultRouter()
router.register(r"inventory", InventoryViewSet, basename="inventory")
urlpatterns = [
path("api/", include(router.urls)),
]Your endpoint is now live at GET /api/inventory/. Callers can filter with ?search=category:Electronics, page with ?limit=20&offset=40, or request a subset of columns with ?fields=sku,name,price.
Optional: a cache layer
If you're expecting bursts of traffic, or if multiple users hit the same filtered view, you don't want every request firing a fresh HTTP call to SheetsAPI. Django's cache framework makes it trivial to add a short TTL.
Add a Redis or Memcached backend to settings.py:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
}
}Then update SheetsAPIClient.list to check the cache first:
from django.core.cache import cache
import hashlib, json
def list(self, search=None, sort=None, limit=50, offset=0, fields=None):
# Build a deterministic cache key from all params
params = dict(search=search, sort=sort, limit=limit, offset=offset, fields=fields)
raw_key = f"{self.sheet_name}:{json.dumps(params, sort_keys=True)}"
cache_key = "sheetsapi:" + hashlib.md5(raw_key.encode()).hexdigest()
cached = cache.get(cache_key)
if cached is not None:
return cached
# ... same requests.get call as before ...
result = response.json()
cache.set(cache_key, result, timeout=60) # 60-second TTL
return resultSixty seconds is enough to absorb a spike without serving stale inventory data for long. Tune the TTL to match how frequently your sheet changes - a product catalog updated once a day can afford a longer window.
If you need to invalidate the cache on demand (say, after a manual data update), store the keys in a set and call cache.delete_many(), or use a cache prefix you can bust with cache.clear() in development.
Try it
Start Django and hit the endpoint:
python manage.py runserver
# All inventory items
curl http://localhost:8000/api/inventory/
# Filter by category
curl "http://localhost:8000/api/inventory/?search=category:Electronics"
# Sort by price descending, first page of 10
curl "http://localhost:8000/api/inventory/?sort=-price&limit=10&offset=0"The response shape matches what SheetsAPI returns directly:
{
"data": [
{
"sku": "KB-002",
"name": "Mechanical Keyboard",
"category": "Electronics",
"price": "129.00",
"stock": "38",
"active": "true"
}
],
"meta": { "total": 2, "limit": 10, "offset": 0 }
}You can deserialize price and stock to their proper types using a DRF serializer if downstream consumers expect numbers rather than strings. Everything else - authentication, filtering, pagination - is handled by the ViewSet and the service class.
Get started with GKit
If you don't have a GKit account yet, sign up at gkit.io - the free tier covers enough requests to build and test this pattern end to end. Grab your user key, generate a bearer token, and you can have a working Django endpoint reading live Sheet data in under an hour.