All posts
Blog

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.

3 min read

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.

Share