公開日: 2026/01/11
簡単なサービスの場合Googleスプレッドシートを使えば、0円で簡単に作成できます。
「え、Excelみたいなやつでしょ?」と思ったあなた、半分正解です。でも、スプレッドシートは本物のデータベースとして使えるんです。
まずは0円でサービスを運用してみて、個人開発がどんな感じかを体感してみましょう。
自分の作りたいサービスの構想をAIに伝えて、どんなDBの構造が良いか提案してもらいましょう。
# プロンプト例
〇〇というサービスを作りたいと思っています。Google spread sheetをDBとして構築したいと思っていますので、どういったデータ構造にすれば良いか提案して下さい。わからないことがあればユーザーに聞き返して下さい。
まず、必要な「道具」を揃えます。
プログラミングの世界では、これを**「ライブラリ」とか「パッケージ」**と言います。
npm install google-spreadsheet google-auth-library
これは何をしているの?
npm(エヌピーエム):Node.jsというプログラミング環境で、便利な道具を手に入れるためのツールgoogle-spreadsheet:Googleスプレッドシートを操作するための道具google-auth-library:Googleで本人確認(認証)するための道具「環境変数」って何?
パスワードや秘密の鍵など、人に見られたくない情報を保存する場所です。
プログラムのコードの中に直接書いちゃうと、GitHubなどにアップロードしたときに、世界中の人に見られてしまいます。
だから、別の安全な場所(環境変数)に保存しておくんです。
// src/lib/env.ts
interface Env {
GOOGLE_SERVICE_ACCOUNT_EMAIL: string;
GOOGLE_PRIVATE_KEY: string;
GOOGLE_SHEET_ID: string;
}
export function getEnv(): Env {
const email = process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL;
const privateKey = process.env.GOOGLE_PRIVATE_KEY;
const sheetId = process.env.GOOGLE_SHEET_ID;
if (!email || !privateKey || !sheetId) {
throw new Error("必要な環境変数が設定されていません");
}
return {
GOOGLE_SERVICE_ACCOUNT_EMAIL: email,
GOOGLE_PRIVATE_KEY: privateKey.replace(/\\n/g, "\n"),
GOOGLE_SHEET_ID: sheetId,
};
}
一行ずつ解説:
interface Env {
GOOGLE_SERVICE_ACCOUNT_EMAIL: string;
GOOGLE_PRIVATE_KEY: string;
GOOGLE_SHEET_ID: string;
}
interface(インターフェース):データの「型」を決めるconst email = process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL;
process.env:環境変数を読み込む場所const:「定数」という意味。一度決めたら変更しない値if (!email || !privateKey || !sheetId) {
throw new Error("必要な環境変数が設定されていません");
}
if (!email || !privateKey || !sheetId):もし3つのうち1つでも無かったらthrow new Error:エラーを出して、プログラムを止めるGOOGLE_PRIVATE_KEY: privateKey.replace(/\\n/g, "\n"),
replace(/\\n/g, "\n"):文字列の中の\\nを\nに置き換える\\nという形式で保存されてしまう「クラス」って何?
クラスは、関連する機能をひとまとめにしたものです。
例えるなら:
今回は「Googleスプレッドシートを操作する」ための機能をまとめたクラスを作ります。
// src/lib/google-sheets.ts
import { GoogleSpreadsheet } from "google-spreadsheet";
import { JWT } from "google-auth-library";
import { getEnv } from "@/src/lib/env";
class GoogleSheetsClient {
private doc: GoogleSpreadsheet;
private jwt: JWT;
private docLoaded: boolean = false;
constructor() {
const env = getEnv();
this.jwt = new JWT({
email: env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n"),
scopes: ["https://www.googleapis.com/auth/spreadsheets.readonly"],
});
this.doc = new GoogleSpreadsheet(env.GOOGLE_SHEET_ID, this.jwt);
}
private async ensureDocLoaded(): Promise<void> {
if (!this.docLoaded) {
await this.doc.loadInfo();
this.docLoaded = true;
}
}
}
一行ずつ解説:
import { GoogleSpreadsheet } from "google-spreadsheet";
import { JWT } from "google-auth-library";
import:他のファイルや、インストールした道具(ライブラリ)を読み込むGoogleSpreadsheet:スプレッドシートを操作するための道具JWT:認証(本人確認)をするための道具「JWT」って何?
class GoogleSheetsClient {
private doc: GoogleSpreadsheet;
private jwt: JWT;
private docLoaded: boolean = false;
class GoogleSheetsClient:「GoogleSheetsClient」という名前のクラスを作るprivate doc:このクラスの中だけで使う変数(外からは見えない)doc:スプレッドシートのドキュメント(書類)jwt:認証用のトークンdocLoaded:「ドキュメントの情報を読み込んだかどうか」を記録するフラグ(旗)constructor() {
const env = getEnv();
// ...
}
constructor(コンストラクター):クラスを作るときに、最初に実行される特別な関数this.jwt = new JWT({
email: env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n"),
scopes: ["https://www.googleapis.com/auth/spreadsheets.readonly"],
});
new JWT:JWTという認証用のトークンを新しく作るemail:Googleのサービスアカウント(専用アカウント)のメールアドレスkey:秘密の鍵scopes:「このトークンで何ができるか」を指定する「scopes」の意味:
spreadsheets.readonly:スプレッドシートを「読み取り専用」で使うprivate async ensureDocLoaded(): Promise<void> {
if (!this.docLoaded) {
await this.doc.loadInfo();
this.docLoaded = true;
}
}
下記の非同期処理については、こちらを参考にして下さい。 PromiseとAsync/awaitを今度こそ完全に理解する【非同期処理】 Api calls in React JS Using async/await inside a React functional component
async(アシンク):非同期処理await(アウェイト):「待つ」という意味Promise(プロミス):「後で結果を返すよ」という約束
または、これらの記事をAIに読み込んでもらい、質問することで理解を深めるのもありでしょう。なぜ「ensureDocLoaded」が必要?
毎回スプレッドシートの情報を読み込むと遅くなるので、一度だけ読み込んで、それを使い回すためです。
「メソッド」って何?
クラスの中にある関数のことです。
async getSummaryData(): Promise<SummaryRow[]> {
try {
await this.ensureDocLoaded();
const sheet = this.doc.sheetsByTitle["summary"];
if (!sheet) {
throw new Error('シート "summary" が見つかりません');
}
const rows = await sheet.getRows();
return rows.map((row) => ({
key: String(row.get("key") || ""),
value: parseNumber(row.get("value")),
label: String(row.get("label") || ""),
}));
} catch (error) {
throw new Error(`サマリーデータの取得に失敗しました: ${error.message}`);
}
}
一行ずつ解説:
try {
// ここに処理を書く
} catch (error) {
// エラーが起きたときの処理
}
「try-catch」って何?
エラーが起きても、プログラムが止まらないようにする仕組みです。
例えるなら:
try {
// スプレッドシートからデータを取る(失敗するかも)
} catch (error) {
// 失敗したら「データが取れませんでした」と表示する
}
なぜ必要?
const sheet = this.doc.sheetsByTitle["summary"];
sheetsByTitle:シート名でシートを探す["summary"]:「summary」という名前のシートを取得const rows = await sheet.getRows();
getRows():シートの全ての行を取得するawaitで、取得が終わるまで待つreturn rows.map((row) => ({
key: String(row.get("key") || ""),
value: parseNumber(row.get("value")),
label: String(row.get("label") || ""),
}));
「map」って何?
配列(リスト)の各要素に対して、同じ処理を繰り返す機能です。
例:
[1, 2, 3, 4, 5]
これに対して「それぞれ2倍にする」という処理をすると:
[1, 2, 3, 4, 5].map(num => num * 2)
// 結果: [2, 4, 6, 8, 10]
今回のコードでは:
row.get("key") || ""
row.get("key"):その行の「key」列の値を取得|| "":もし値がなければ、空文字("")を使う「||」って何?
A || Bは「Aがあればそれを使う、なければBを使う」export async function getDashboardData(): Promise<DashboardData> {
const client = new GoogleSheetsClient();
const [summary, projects, trends] = await Promise.all([
client.getSummaryData(),
client.getProjectsData(),
client.getTrendsData(),
]);
return { summary, projects, trends };
}
「Promise.all」って何?
複数の処理を同時に実行して、全部終わるのを待つ機能です。
普通のやり方(遅い):
const summary = await client.getSummaryData(); // 1秒かかる
const projects = await client.getProjectsData(); // 1秒かかる
const trends = await client.getTrendsData(); // 1秒かかる
// 合計3秒
Promise.allを使う(速い):
const [summary, projects, trends] = await Promise.all([
client.getSummaryData(), // 同時に開始
client.getProjectsData(), // 同時に開始
client.getTrendsData(), // 同時に開始
]);
// 合計1秒(一番遅いものに合わせる)
3つの処理を同時にやるので、3倍速くなります!
const [summary, projects, trends] = ...
これは何?
配列の「分割代入」という機能です。
例:
const numbers = [1, 2, 3];
const [a, b, c] = numbers;
// a = 1, b = 2, c = 3
「Next.js」って何?
ReactというJavaScriptのフレームワーク(便利な道具セット)を使って、Webサイトを作るためのツールです。
簡単に言うと:「Webサイトを作るための、すごく便利なテンプレート」です。
// app/page.tsx
import { getDashboardPageData } from "@/src/lib/dashboard";
export const revalidate = 10800;
export default async function DashboardPage() {
const { kpiData, monthlyData, projects } = await getDashboardPageData();
return (
<div>
<h1>ダッシュボード</h1>
{/* ここにデータを表示 */}
</div>
);
}
一行ずつ解説:
export const revalidate = 10800;
「revalidate」って何?
データを再検証(更新)する間隔を秒数で指定します。
10800秒 = 3時間なぜこれが必要?
毎回スプレッドシートにアクセスすると:
だから、一度取得したデータを3時間キャッシュ(保存)しておいて、3時間経ったら新しいデータを取り直します。
これを**「ISR(Incremental Static Regeneration)」**と言います。
「ISR」って何?
簡単に言うと:
export async function getDashboardPageData(): Promise<DashboardPageData> {
const [dashboardDataResult] = await Promise.allSettled([
getDashboardData().catch(() => null),
]);
let kpiData: KPIData = fallbackKPIData;
let monthlyData: MonthlyDataPoint[] = fallbackMonthlyData;
let projects: Project[] = fallbackProjects;
if (dashboardDataResult.status === "fulfilled" && dashboardDataResult.value) {
const dashboardData = dashboardDataResult.value;
kpiData = convertSummaryToKPIData(dashboardData.summary);
monthlyData = convertTrendsToMonthlyData(dashboardData.trends);
projects = convertProjectsToProjectData(dashboardData.projects);
}
return { kpiData, monthlyData, projects };
}
「Promise.allSettled」って何?
Promise.allと似ていますが、違いがあります。
Promise.all:1つでも失敗したら、全部失敗扱いPromise.allSettled:失敗したものがあっても、成功したものは取得できる「fallback(フォールバック)」って何?
「失敗したときの予備」という意味です。
例えば:
このダミーデータが「フォールバックデータ」です。
if (dashboardDataResult.status === "fulfilled") {
// データ取得が成功した場合の処理
}
status === "fulfilled":「成功」という意味症状:
プログラムを実行すると、ERR_OSSL_UNSUPPORTEDというエラーが出る。
原因: 秘密鍵の改行文字が正しく処理されていない。
解決方法:
key: env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n")
この一行を追加することで、\\nを実際の改行文字\nに変換します。
症状: 日付が1日ずれて表示される。
原因:
JavaScriptのnew Date()は、UTCタイムゾーン(世界標準時)で日付を解釈します。日本時間(JST)は、UTCより9時間進んでいるため、ずれが生じます。
解決方法:
export function formatDate(dateStr: string): string {
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
const [year, month, day] = dateStr.split("-");
const date = new Date(
parseInt(year, 10),
parseInt(month, 10) - 1,
parseInt(day, 10)
);
return date.toLocaleDateString("ja-JP", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
}
}
症状: 特定のシートが存在しないとき、エラーが出てページ全体が表示されない。
解決方法: エラーを投げずに、空配列を返す。
async getOptionalData(): Promise<DataRow[]> {
try {
await this.ensureDocLoaded();
const sheet = this.doc.sheetsByTitle["optional"];
if (!sheet) {
return []; // エラーを投げずに、空配列を返す
}
const rows = await sheet.getRows();
return rows.map((row) => ({ /* データを整形 */ }));
} catch (error) {
console.error("データ取得失敗:", error);
return []; // エラーが起きても、空配列を返す
}
}
Googleスプレッドシートをデータベースとして使うのは、個人開発者にとって最高の選択肢の1つです。
まずは小さく始めて、必要になったら本格的なデータベースに移行すればOKです。
さらに深く学びたい人のために、役立つリンクを紹介します。
Google Sheets API 公式ドキュメント(日本語)
google-spreadsheet(npm)
Next.js 公式ドキュメント(日本語)
Google Sheets APIの使い方を丁寧に解説
Next.jsでISRを使う方法
Googleスプレッドシートをデータベースとして使うサンプルプロジェクト
個人開発者のためのデータベース選択ガイド
今日から、あなたもGoogleスプレッドシートでアプリ開発を始めてみませんか?
わからないことがあれば、コメント欄で質問してください。一緒に学んでいきましょう!