Use Google Sheets as a Database in Next.js

Use Google Sheets as a Database in Next.js

Sufi Aurangzeb Hossain
February 8, 2025
27 min read
Next.jsBackend

Google Sheets provides a powerful, free, and user-friendly alternative to traditional databases for managing content in Next.js applications. This approach enables non-technical users to update application content without code changes, making it ideal for blogs, product catalogs, event listings, and dynamic content management.

Using Google Sheets as a Dynamic Data Source for Next.js Applications

Google Sheets provides a powerful, free, and user-friendly alternative to traditional databases for managing content in Next.js applications. This approach enables non-technical users to update application content without code changes, making it ideal for blogs, product catalogs, event listings, and dynamic content management.

Overview: Public CSV Export Method

This method leverages Google Sheets' built-in CSV export functionality, requiring no authentication for publicly accessible sheets. It's perfect for read-only data that needs frequent updates by content managers.

Step 1: Preparing Your Google Sheet for Public Access

Configure your Google Sheet properly to enable public CSV export.

Configuration Steps:

  1. Open your Google Sheet in the browser
  2. Click File → Share → Share with others
  3. Set General access to Anyone with the link
  4. Choose Viewer permission level
  5. Copy the generated shareable link
  6. Ensure the sheet contains properly formatted data with headers
Original Shareable URL:
https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit?usp=sharing

Step 2: Extracting Sheet ID and GID Parameters

Understand and extract the necessary identifiers from your Google Sheet URL.

Sheet ID Extraction:

  • Locate the string between /d/ and /edit in your sheet URL
  • Example: From https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit
  • Sheet ID: YOUR_SHEET_ID

GID (Grid ID) Identification:

  • GID identifies specific tabs/worksheets within a spreadsheet
  • Default GID is usually 0 for the first tab
  • Find GID by clicking on a tab and checking the URL: ...edit#gid=YOUR_GID
  • Multiple tabs require different GID values for data access

Step 3: Constructing the CSV Export URL

Build the proper CSV export URL using the extracted identifiers.

CSV Export URL Template:
https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/export?format=csv&gid=YOUR_GID
Practical Example:
https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/export?format=csv&gid=0

Step 4: Next.js Implementation with Type Safety

Implement a robust data fetching utility with proper TypeScript support.

// lib/utils/google-sheets.ts
import Papa from 'papaparse';

export interface SheetConfig {
  sheetId: string;
  gid: string;
}

export interface SheetRow {
  [key: string]: string;
}

export class GoogleSheetsError extends Error {
  constructor(
    message: string,
    public statusCode?: number,
    public originalError?: unknown
  ) {
    super(message);
    this.name = 'GoogleSheetsError';
  }
}

export async function fetchGoogleSheet<T>({
  sheetId,
  gid = '0',
  transform,
  cacheTime = 3600, // 1 hour default cache
}: SheetConfig & {
  transform?: (row: SheetRow) => T;
  cacheTime?: number;
}): Promise<T[]> {
  const csvUrl = `https://docs.google.com/spreadsheets/d/${sheetId}/export?format=csv&gid=${gid}`;
  
  try {
    const response = await fetch(csvUrl, {
      next: { 
        revalidate: cacheTime,
        tags: [`sheet-${sheetId}-${gid}`],
      },
      headers: {
        'User-Agent': 'Next.js Google Sheets Fetcher',
      },
    });
    
    if (!response.ok) {
      throw new GoogleSheetsError(
        `Failed to fetch Google Sheet: ${response.status} ${response.statusText}`,
        response.status
      );
    }
    
    const csvText = await response.text();
    
    // Validate CSV content
    if (!csvText.trim()) {
      throw new GoogleSheetsError('Google Sheet returned empty content');
    }
    
    return new Promise((resolve, reject) => {
      Papa.parse(csvText, {
        header: true,
        skipEmptyLines: true,
        complete: (results) => {
          if (results.errors.length > 0) {
            reject(new GoogleSheetsError(
              `CSV parsing errors: ${results.errors.map(e => e.message).join(', ')}`
            ));
            return;
          }
          
          const data = results.data as SheetRow[];
          
          if (transform) {
            const transformedData = data.map(transform).filter(Boolean) as T[];
            resolve(transformedData);
          } else {
            resolve(data as T[]);
          }
        },
        error: (error: Error) => {
          reject(new GoogleSheetsError(
            `CSV parsing failed: ${error.message}`,
            undefined,
            error
          ));
        },
      });
    });
  } catch (error) {
    if (error instanceof GoogleSheetsError) {
      throw error;
    }
    
    throw new GoogleSheetsError(
      `Unexpected error fetching Google Sheet: ${error instanceof Error ? error.message : 'Unknown error'}`,
      undefined,
      error
    );
  }
}

// Utility function to validate sheet configuration
export function validateSheetConfig(sheetId?: string, gid?: string): void {
  if (!sheetId || sheetId.trim() === '') {
    throw new GoogleSheetsError('Google Sheet ID is required');
  }
  
  if (!gid || gid.trim() === '') {
    throw new GoogleSheetsError('Google Sheet GID is required');
  }
}

Step 5: Environment Configuration

Set up environment variables for secure configuration management.

# .env.local
GOOGLE_SHEETS_ID=your_sheet_id_here
ARTICLES_SHEET_GID=0
PRODUCTS_SHEET_GID=your_products_gid
USERS_SHEET_GID=your_users_gid

# Optional: Cache configuration
GOOGLE_SHEETS_CACHE_TIME=3600

# .env.example (for documentation)
GOOGLE_SHEETS_ID=your_google_sheet_id
ARTICLES_SHEET_GID=0
PRODUCTS_SHEET_GID=0
USERS_SHEET_GID=0
// lib/config/google-sheets.ts
export const googleSheetsConfig = {
  articles: {
    sheetId: process.env.GOOGLE_SHEETS_ID!,
    gid: process.env.ARTICLES_SHEET_GID || '0',
  },
  products: {
    sheetId: process.env.GOOGLE_SHEETS_ID!,
    gid: process.env.PRODUCTS_SHEET_GID || '0',
  },
  users: {
    sheetId: process.env.GOOGLE_SHEETS_ID!,
    gid: process.env.USERS_SHEET_GID || '0',
  },
  cacheTime: parseInt(process.env.GOOGLE_SHEETS_CACHE_TIME || '3600'),
} as const;

// Validation at runtime
if (!process.env.GOOGLE_SHEETS_ID) {
  throw new Error('GOOGLE_SHEETS_ID environment variable is required');
}

Step 6: Server-Side Implementation in Next.js Pages

Implement server-side data fetching in Next.js pages with proper error handling.

// app/articles/page.tsx
import { fetchGoogleSheet, GoogleSheetsError } from '@/lib/utils/google-sheets';
import { googleSheetsConfig } from '@/lib/config/google-sheets';

interface Article {
  id: string;
  title: string;
  content: string;
  author: string;
  publishedAt: string;
  category: string;
  readTime: number;
}

function transformArticleRow(row: any): Article {
  return {
    id: row.id || '',
    title: row.title || 'Untitled',
    content: row.content || '',
    author: row.author || 'Unknown Author',
    publishedAt: row.published_at || new Date().toISOString(),
    category: row.category || 'General',
    readTime: parseInt(row.read_time) || 5,
  };
}

export default async function ArticlesPage() {
  let articles: Article[] = [];
  let error: string | null = null;
  
  try {
    articles = await fetchGoogleSheet({
      ...googleSheetsConfig.articles,
      transform: transformArticleRow,
      cacheTime: googleSheetsConfig.cacheTime,
    });
    
    // Sort articles by publication date (newest first)
    articles.sort((a, b) => 
      new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
    );
  } catch (err) {
    console.error('Failed to fetch articles:', err);
    error = err instanceof GoogleSheetsError 
      ? err.message 
      : 'An unexpected error occurred';
  }
  
  if (error) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <div className="text-center">
          <h1 className="text-2xl font-bold text-red-600 mb-4">
            Failed to Load Articles
          </h1>
          <p className="text-gray-600">{error}</p>
        </div>
      </div>
    );
  }
  
  return (
    <div className="min-h-screen bg-gray-50 py-8">
      <div className="container mx-auto px-4">
        <header className="text-center mb-12">
          <h1 className="text-4xl font-bold text-gray-900 mb-4">
            Latest Articles
          </h1>
          <p className="text-xl text-gray-600 max-w-2xl mx-auto">
            Discover our latest insights and updates powered by Google Sheets
          </p>
        </header>
        
        <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
          {articles.map((article) => (
            <article 
              key={article.id}
              className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300"
            >
              <div className="p-6">
                <div className="flex justify-between items-start mb-3">
                  <span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
                    {article.category}
                  </span>
                  <span className="text-sm text-gray-500">
                    {article.readTime} min read
                  </span>
                </div>
                
                <h2 className="text-xl font-semibold text-gray-900 mb-3 line-clamp-2">
                  {article.title}
                </h2>
                
                <p className="text-gray-600 mb-4 line-clamp-3">
                  {article.content}
                </p>
                
                <div className="flex justify-between items-center text-sm text-gray-500">
                  <span>By {article.author}</span>
                  <time dateTime={article.publishedAt}>
                    {new Date(article.publishedAt).toLocaleDateString()}
                  </time>
                </div>
              </div>
            </article>
          ))}
        </div>
        
        {articles.length === 0 && (
          <div className="text-center py-12">
            <p className="text-gray-500 text-lg">No articles found.</p>
          </div>
        )}
      </div>
    </div>
  );
}

export const dynamic = 'force-static';
export const revalidate = 3600; // Revalidate every hour

Step 7: API Route Implementation

Create API endpoints for client-side data consumption.

// app/api/articles/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { fetchGoogleSheet, GoogleSheetsError } from '@/lib/utils/google-sheets';
import { googleSheetsConfig } from '@/lib/config/google-sheets';

export async function GET(request: NextRequest) {
  try {
    const searchParams = request.nextUrl.searchParams;
    const category = searchParams.get('category');
    const limit = parseInt(searchParams.get('limit') || '50');
    
    const articles = await fetchGoogleSheet({
      ...googleSheetsConfig.articles,
      cacheTime: googleSheetsConfig.cacheTime,
    });
    
    // Filter by category if specified
    let filteredArticles = articles;
    if (category) {
      filteredArticles = articles.filter(
        (article: any) => article.category?.toLowerCase() === category.toLowerCase()
      );
    }
    
    // Apply limit
    const limitedArticles = filteredArticles.slice(0, limit);
    
    return NextResponse.json({
      success: true,
      data: limitedArticles,
      meta: {
        total: articles.length,
        filtered: filteredArticles.length,
        returned: limitedArticles.length,
        category,
        limit,
      },
    });
  } catch (error) {
    console.error('API Error:', error);
    
    if (error instanceof GoogleSheetsError) {
      return NextResponse.json(
        {
          success: false,
          error: error.message,
          code: error.statusCode || 500,
        },
        { status: error.statusCode || 500 }
      );
    }
    
    return NextResponse.json(
      {
        success: false,
        error: 'Internal server error',
      },
      { status: 500 }
    );
  }
}

Step 8: Client-Side Data Fetching

Implement client-side components that consume the API endpoints.

// components/ArticlesList.tsx
'use client';

import { useState, useEffect } from 'react';

interface Article {
  id: string;
  title: string;
  content: string;
  author: string;
  category: string;
}

interface ArticlesResponse {
  success: boolean;
  data: Article[];
  meta?: {
    total: number;
    filtered: number;
    returned: number;
    category?: string;
    limit?: number;
  };
  error?: string;
}

export function ArticlesList({ 
  category,
  limit = 10 
}: { 
  category?: string;
  limit?: number;
}) {
  const [articles, setArticles] = useState<Article[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  
  useEffect(() => {
    async function fetchArticles() {
      try {
        setLoading(true);
        setError(null);
        
        const params = new URLSearchParams();
        if (category) params.append('category', category);
        params.append('limit', limit.toString());
        
        const response = await fetch(`/api/articles?${params}`);
        const result: ArticlesResponse = await response.json();
        
        if (!response.ok || !result.success) {
          throw new Error(result.error || 'Failed to fetch articles');
        }
        
        setArticles(result.data);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An error occurred');
        console.error('Error fetching articles:', err);
      } finally {
        setLoading(false);
      }
    }
    
    fetchArticles();
  }, [category, limit]);
  
  if (loading) {
    return (
      <div className="flex justify-center items-center py-8">
        <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
        <span className="ml-2 text-gray-600">Loading articles...</span>
      </div>
    );
  }
  
  if (error) {
    return (
      <div className="bg-red-50 border border-red-200 rounded-lg p-4 text-center">
        <p className="text-red-800">Error: {error}</p>
        <button 
          onClick={() => window.location.reload()}
          className="mt-2 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
        >
          Try Again
        </button>
      </div>
    );
  }
  
  return (
    <div className="space-y-6">
      {articles.map((article) => (
        <div 
          key={article.id}
          className="border border-gray-200 rounded-lg p-6 hover:shadow-md transition-shadow"
        >
          <div className="flex justify-between items-start mb-3">
            <span className="inline-block bg-gray-100 text-gray-800 text-sm px-3 py-1 rounded-full">
              {article.category}
            </span>
          </div>
          
          <h3 className="text-xl font-semibold text-gray-900 mb-2">
            {article.title}
          </h3>
          
          <p className="text-gray-600 mb-4 line-clamp-2">
            {article.content}
          </p>
          
          <div className="text-sm text-gray-500">
            By {article.author}
          </div>
        </div>
      ))}
      
      {articles.length === 0 && (
        <div className="text-center py-8 text-gray-500">
          No articles found{category ? ` in category "${category}"` : ''}.
        </div>
      )}
    </div>
  );
}

Advanced Features and Optimization

Implement advanced features for better performance and user experience.

// lib/utils/sheets-cache.ts
import { unstable_cache } from 'next/cache';
import { fetchGoogleSheet } from './google-sheets';
import { googleSheetsConfig } from '@/lib/config/google-sheets';

// Cached version with stable caching
export const getCachedArticles = unstable_cache(
  async () => {
    return await fetchGoogleSheet({
      ...googleSheetsConfig.articles,
      cacheTime: googleSheetsConfig.cacheTime,
    });
  },
  ['articles-data'],
  {
    revalidate: googleSheetsConfig.cacheTime,
    tags: ['articles'],
  }
);

export const getCachedProducts = unstable_cache(
  async () => {
    return await fetchGoogleSheet({
      ...googleSheetsConfig.products,
      cacheTime: googleSheetsConfig.cacheTime,
    });
  },
  ['products-data'],
  {
    revalidate: googleSheetsConfig.cacheTime,
    tags: ['products'],
  }
);

// lib/hooks/useGoogleSheets.ts
import { useQuery } from '@tanstack/react-query';

export function useGoogleSheet<T>({
  sheetId,
  gid,
  enabled = true,
}: {
  sheetId: string;
  gid: string;
  enabled?: boolean;
}) {
  return useQuery({
    queryKey: ['google-sheet', sheetId, gid],
    queryFn: async () => {
      const response = await fetch(`/api/sheets?sheetId=${sheetId}&gid=${gid}`);
      if (!response.ok) {
        throw new Error('Failed to fetch sheet data');
      }
      return response.json() as Promise<T[]>;
    },
    enabled,
    staleTime: 5 * 60 * 1000, // 5 minutes
    cacheTime: 10 * 60 * 1000, // 10 minutes
  });
}

Security Considerations and Best Practices

Ensure your implementation follows security best practices.

Security Guidelines:

  • Only use with public, non-sensitive data
  • Implement rate limiting for API routes
  • Validate and sanitize all data from Google Sheets
  • Use environment variables for configuration
  • Implement proper error handling without information leakage
  • Consider CORS policies for client-side requests
  • Monitor for unexpected data formats or sizes

Performance Optimization:

  • Implement proper caching strategies
  • Use incremental static regeneration (ISR)
  • Optimize bundle size by lazy loading
  • Implement proper loading states
  • Consider data pagination for large datasets

Real-time Updates and Webhooks

Implement real-time updates for instant content synchronization.

// pages/api/webhook/sheets-update.js (for older Next.js versions)
// Configure Google Apps Script to send webhooks on sheet changes

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }
  
  // Verify webhook secret (optional but recommended)
  const webhookSecret = process.env.WEBHOOK_SECRET;
  if (webhookSecret && req.headers['x-webhook-secret'] !== webhookSecret) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  
  try {
    const { sheetId, gid } = req.body;
    
    // Revalidate the specific page or data
    if (sheetId && gid) {
      await res.revalidate(`/data/${sheetId}/${gid}`);
    }
    
    // Clear relevant caches
    await res.revalidateTag(`sheet-${sheetId}-${gid}`);
    
    res.json({ 
      success: true, 
      revalidated: true,
      timestamp: new Date().toISOString() 
    });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({ 
      success: false, 
      error: 'Failed to process webhook' 
    });
  }
}

Benefits and Use Cases

Understand the advantages and ideal scenarios for this approach.

Key Benefits:

  • Cost-Effective: No database hosting costs
  • User-Friendly: Non-technical users can manage content
  • Real-time Updates: Changes reflect immediately
  • Version Control: Google Sheets maintains revision history
  • Collaboration: Multiple editors can work simultaneously
  • Accessibility: Accessible from any device with a browser

Ideal Use Cases:

  • Blog and article management
  • Product catalogs and inventories
  • Event calendars and listings
  • Team member directories
  • FAQ and knowledge base content
  • Dynamic pricing tables
  • Content-heavy marketing sites

This comprehensive approach to using Google Sheets with Next.js provides a robust, scalable solution for dynamic content management that bridges the gap between technical implementation and content editing, enabling seamless collaboration between developers and content creators.