Using Google Sheets as a Mobile App Backend
How to use GKit SheetsAPI as a lightweight data layer for React Native and Flutter apps — no server required.
Why a spreadsheet makes sense for simple mobile apps
Most mobile apps start with modest data needs: a list of locations, a product catalog, a schedule someone updates manually every week. Standing up a backend server, a database, and a deployment pipeline for that is real overhead — and if the person keeping the data fresh is a non-technical teammate, a spreadsheet is genuinely the right tool for them.
GKit SheetsAPI turns any Google Sheet into a REST API over HTTPS. CORS is open, the JSON shape is predictable, and the endpoint needs no server you maintain. For read-heavy mobile apps with small datasets — under 10,000 rows, edited by humans — it covers everything a dedicated backend would.
SheetsAPI is currently in beta and free while we test it.
React Native: fetching rows with useEffect
The endpoint is a plain GET, so the native fetch API works fine — no extra libraries needed.
// hooks/useSheetData.js
import { useEffect, useState } from "react";
const BASE = "https://sheetsapi.gkit.mreshank.com/api";
export function useSheetData(userKey, sheetName, params = {}) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const query = new URLSearchParams({ limit: "200", ...params }).toString();
const url = `${BASE}/spreadsheets/${userKey}/${sheetName}?${query}`;
fetch(url)
.then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then((rows) => {
setData(rows);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, [userKey, sheetName]);
return { data, loading, error };
}Use it in a screen component:
// screens/LocationsScreen.jsx
import { FlatList, Text, View } from "react-native";
import { useSheetData } from "../hooks/useSheetData";
export default function LocationsScreen() {
const { data, loading, error } = useSheetData("YOUR_USER_KEY", "Locations");
if (loading) return <Text>Loading…</Text>;
if (error) return <Text>Could not load locations: {error}</Text>;
return (
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View>
<Text>{item.name}</Text>
<Text>{item.address}</Text>
</View>
)}
/>
);
}The first row of your sheet defines the field names — id, name, address become the keys on each JSON object.
Flutter: fetching rows with http
Add the http package to pubspec.yaml, then a simple async fetch covers it:
// lib/services/sheets_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
class SheetsService {
static const _base = 'https://sheetsapi.gkit.mreshank.com/api';
static Future<List<Map<String, dynamic>>> fetchRows(
String userKey,
String sheetName, {
int limit = 200,
}) async {
final uri = Uri.parse(
'$_base/spreadsheets/$userKey/$sheetName?limit=$limit',
);
final response = await http.get(uri);
if (response.statusCode != 200) {
throw Exception('Failed to load sheet: ${response.statusCode}');
}
final List<dynamic> raw = jsonDecode(response.body);
return raw.cast<Map<String, dynamic>>();
}
}Then in a widget:
// lib/screens/locations_screen.dart
FutureBuilder<List<Map<String, dynamic>>>(
future: SheetsService.fetchRows('YOUR_USER_KEY', 'Locations'),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
final rows = snapshot.data!;
return ListView.builder(
itemCount: rows.length,
itemBuilder: (_, i) => ListTile(
title: Text(rows[i]['name'] ?? ''),
subtitle: Text(rows[i]['address'] ?? ''),
),
);
},
)Offline considerations
SheetsAPI does not have a built-in offline layer — the network call either succeeds or fails. For a mobile app that needs to work without connectivity, cache the last successful response locally. In React Native, AsyncStorage works well:
async function loadWithCache(url, cacheKey) {
try {
const response = await fetch(url);
const json = await response.json();
await AsyncStorage.setItem(cacheKey, JSON.stringify(json));
return json;
} catch {
const cached = await AsyncStorage.getItem(cacheKey);
return cached ? JSON.parse(cached) : [];
}
}The pattern is the same in Flutter with shared_preferences or hive.
When to graduate to a real backend
SheetsAPI is the right call when data is small and human-edited. These are the signs you've outgrown it:
- Concurrent writes at any volume. Sheets has no row-level locking. Two writes at the same moment can overwrite each other.
- More than ~50,000 rows. Reads slow down noticeably beyond this, and Google's own API limits start to bite.
- Relational data. JOINs across tabs are not supported — you'd need to fetch two sheets and join them in the app.
- User authentication. SheetsAPI is public by default (or key-gated). For per-user data, you need a backend that understands identity.
For a catalog, a schedule, a leaderboard, or a list of locations managed by hand — a spreadsheet and SheetsAPI ship faster than anything else. When the app outgrows it, the migration to a real database is mechanical: the JSON shape your app already consumes maps directly to a table schema.