家計簿アプリを作る #25:収支データのCSVエクスポート
家計簿アプリ作成シリーズの第25回です。月別の収支データをCSVファイルとしてダウンロードできる機能を実装します。
実装方針
SvelteKit の Form Action ではファイルダウンロードが難しいため、+server.ts(APIエンドポイント)を使います。GET リクエストでCSVを生成して返し、+page.svelte からは通常のリンクでアクセスします。
APIエンドポイントの配置
src/routes/(auth)/api/export/+server.ts
(auth) グループ内に配置することで、(auth)/+layout.server.ts の認証チェックが効きます。(auth) の外に置くと認証チェックが効かなくなるため注意が必要です。
+server.ts の実装
import type { RequestHandler } from "./$types";
import { createDb } from '$lib/server/db';
import { transactions } from '$lib/server/db/schema';
import { eq, and, like } from 'drizzle-orm';
export const GET: RequestHandler = async ({ locals, platform, url }) => {
const selectedMonth = url.searchParams.get('month') ?? '';
const db = createDb(platform!.env.DB);
const monthTransactions = await db.select().from(transactions)
.where(
and(
eq(transactions.userId, locals.user!.id),
like(transactions.date, `${selectedMonth}%`)
)
);
const header = '日付,種類,カテゴリ,メモ,金額';
const rows = monthTransactions.map(t =>
`${t.date},${t.type === 'income' ? '収入' : '支出'},${t.category},${t.memo},${t.amount}`
).join('\n');
const csv = `${header}\n${rows}`;
return new Response(csv, {
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': `attachment; filename="transactions-${selectedMonth}.csv"`
}
});
};
Content-Disposition: attachment を指定することでブラウザがファイルとしてダウンロードします。filename= で保存時のファイル名を指定します。
CSVのヘッダ行
スキーマからDB列名を取得することも技術的には可能ですが、user_id や amount などの英語列名がそのまま出てしまうため、日本語ヘッダは自分で定義する方が実用的です。
+page.svelte の実装
Button に href を渡すだけでリンクとして動作します。
<Button href={`/api/export?month=${data.selectedMonth}`}>CSVエクスポート</Button>
ハマりポイント:テンプレートリテラルのミス
// ❌ $が抜けるとファイル名に } が混入する
href={`/api/export?month=${data.selectedMonth}`} // → /api/export?month=2026-06}
// ✅ 正しい
href={`/api/export?month=${data.selectedMonth}`}
まとめ
| ポイント | 内容 |
|---|---|
| ファイルダウンロード | Form Action ではなく +server.ts の GET で実装 |
| 認証チェック | (auth) グループ内に配置して認証を共通化 |
| CSVヘッダ | スキーマからではなく日本語で自前定義 |
| ダウンロード | Content-Disposition: attachment でブラウザにファイル保存させる |