UpstashとNext.jsで実現するSaaS向けの信頼性の高いクォータシステム構築ガイド
この記事では、Next.jsとPrismaで構築されたSaaSアプリケーション向けに、Upstashを活用したクォータシステムの実装方法を解説します。APIの作成にはNext.jsのAPIルートを使用します。
クォータシステムとは
多くのSaaSアプリケーションでは、クォータシステムが採用されています。これは、一定期間内にユーザーが実行できるアクションの回数を制限する仕組みです。
本記事の例では、「Free」プランのユーザーは月に1,000リクエストまでAPIを利用できます。1,000件を超えるAPIリクエストを送信しようとすると、アプリケーションはそれ以上のリクエストをブロックします。
これらのAPIリクエストは、スプレッドシートの内容を取得してJSONに変換するために使用されます。fastsheetはまさにこれを実現するサービスで、GoogleスプレッドシートをJSON APIへと変換できます。

データベーススキーマの定義
前述のとおり、データベースとのやり取りにはPrismaをORMとして使用します。以下は、クォータシステムを実装したUserモデルと、スプレッドシートを保存するためのSpreadsheetモデルの例です。
model User {
id Int @id @default(autoincrement())
planId String @default("FREE")
email String @unique
quota Int @default(0)
spreadsheets Spreadsheet[]
}
model Spreadsheet {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
content String
}
planIdフィールドは、ユーザーが契約しているプランを表します。quotaフィールドは、当月中にユーザーが実行したAPIリクエストの回数です。Spreadsheetモデルはスプレッドシートを表し、Userに関連付けられています。- 1人の
Userには、複数のSpreadsheetを紐付けることができます。
ここでは話をシンプルにするため、月に1,000リクエストまで利用できる「FREE」プランのみを扱います。
ユーザーのクォータを増加させる
ユーザーがAPIリクエストを行うたびに、そのクォータを1ずつ増やしていきたいところです。
一見すると簡単そうに思えます。Next.jsのAPIルート上で、ユーザーがAPIリクエストを行うたびにクォータを1増やせばよいだけです。
// pages/api/spreadsheets/[id]/index.ts
import type { NextApiRequest, NextApiResponse } from "next";
const MAX_FREE_TIER_QUOTA = 1000;
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
// URLからスプレッドシートのIDを取得
const { id } = req.params;
// データベースからスプレッドシートと関連するユーザーを取得
const spreadsheet = await prisma.spreadsheet.findUnique({
where: { id: Number(id) },
include: { user: true },
});
// スプレッドシートが存在するか確認
if (!spreadsheet) {
return res.status(404).json({ error: "Spreadsheet not found" });
}
// ユーザーがクォータ上限を超えていないか確認
if (spreadsheet.user.quota >= MAX_FREE_TIER_QUOTA) {
return res.status(429).json({ error: "Quota exceeded" });
}
// ユーザーのクォータを1増やす
await prisma.user.update({
where: { id: spreadsheet.user.id },
data: { quota: { increment: 1 } },
});
// スプレッドシートの内容を返す
return res.json({ content: spreadsheet.content });
}
しかしこのアプローチには問題があります。期待ほど高速ではないのです。
APIリクエストごとに、クォータを増やすためのデータベーストランザクションが発生してしまいます。
ユーザーのクォータを取得するためにデータベースへアクセスし、クォータを1増やすトランザクションを作成してから、スプレッドシートの内容を返すという流れになります。
この方法が最適でない理由はいくつかあります。
- データベースが遅い場合、レスポンスタイムが低下します。
- データベースがサーバーから遠い場所にある場合も、レスポンスタイムが低下します。
- このコードをEdge互換にするのは難しく、世界中に複数のデータベースレプリカを配置する必要があります(これは簡単な作業ではありません!)。
Upstashでユーザーのクォータを管理する
この問題を解決するために、Upstash Redis®を使ってユーザーのクォータを管理します。Upstash Redis®は、クラウド上でホストされる高速かつ信頼性の高いRedis®データベースで、サーバーレス環境向けに設計されており、Edge(ユーザーの近くでコードを実行すること)にも対応しています。
ユーザーのクォータを1増やすには、Redis®のINCRコマンドを使用します。このコマンドはアトミックに動作するため、確実に一度だけ実行され、しかも高速です。
以下は、Upstash Redis®でクォータを管理するように書き換えた同じコードです。
// pages/api/spreadsheets/[id]/index.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { Redis } from "@upstash/redis";
const MAX_FREE_TIER_QUOTA = 1000;
// 環境変数を使ってUpstash Redisインスタンスを生成
// .envファイルへの定義を忘れずに
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
// URLからスプレッドシートのIDを取得
const { id } = req.params;
// データベースからスプレッドシートと関連するユーザーを取得
const spreadsheet = await prisma.spreadsheet.findUnique({
where: { id: Number(id) },
include: { user: true },
});
// スプレッドシートが存在するか確認
if (!spreadsheet) {
return res.status(404).json({ error: "Spreadsheet not found" });
}
// Redisのクォータキー。ユーザーIDごとに一意
const quotaKey = `user:${spreadsheet.user.id}:quota`;
// Redisからユーザーのクォータを取得(INCRで1増加)
const quota = await redis.incr(quotaKey);
// キーが存在しなかった場合、戻り値は1になるため
// 有効期限を1日に設定する
if (quota === 1) {
await redis.expire(quotaKey, 60 * 60 * 24);
}
// ユーザーがクォータ上限を超えていないか確認
if (quota > MAX_FREE_TIER_QUOTA) {
return res.status(429).json({ error: "Quota exceeded" });
}
// スプレッドシートの内容を返す
return res.json({ content: spreadsheet.content });
}
Upstash Redis®を利用すれば、Upstash Redis® Edgeの力を借りてユーザーの近くでコードを実行でき、コードのスケーラビリティも容易に確保できます。
まとめ
Upstash Redis®を使えば、サーバーレス環境向けのキャッシュシステムを非常に簡単に実装できます。
今回はクォータシステムの実装に使いましたが、この考え方は他のさまざまなユースケースにも応用できます。たとえば、データベースクエリの結果をキャッシュしたり、APIリクエストの結果をキャッシュしたりすることも可能です。
キャッシュの最終的な目的は、APIを高速化し、リソース消費(例:PlanetScaleデータベースの読み書きクエリ)を抑えることにあります。
今後の記事では、Upstash QStashを使ってRedis®からユーザーのクォータを取得し、データベースに保存する方法を紹介します。これにより、ユーザーのクォータが常に最新の状態に保たれ、失われる心配がなくなります。
実際に試してみる
このような最適化を実際に体験したい方は、fastsheetをぜひチェックしてみてください。Googleスプレッドシートを数クリックでAPIに変換できるAPIサービスで、充実した無料枠付きで無料で利用できます。
-
Grafana用Redisデータソースプラグイン徹底解説:導入から監視ダッシュボード構築まで
Grafanaは、広く知られ多くの現場で活用されているオープンソースのアプリケーション監視ツールです。そして現在、新しい「Grafana用Redisデータソース」プラグインの登場により、GrafanaでRedisを直接扱えるようになりました。 この新機能により、DevOpsエンジニアやデータベース管理者は、すでに使い慣れたツールをそのまま活用して、Redisデータベースやアプリケーションデータを監視するダッシュボードを簡単に作成できます。新しいGrafana用Redisデータソースプラグインでは、RedisTimeSeriesのデータや、Strings(文字列)、Hashes(ハッシュ)、S
-
SQLパフォーマンスを劇的に向上:Drizzle ORMクエリのためのUpstash Redisキャッシュ活用法
先日、私たちはDrizzle ORMとのコラボレーションを実現する機会に恵まれました。 TypeScript ORMとしてコミュニティから絶大な支持を集めるDrizzle ORM。そのため、「はい 😳」と答えるのは簡単な決断でした: 本記事では、Upstash Redis × Drizzleのキャッシング統合がどのようにSQLパフォーマンスを向上させるのか、そしてLuaスクリプトとハッシュデータ構造を活用してこの統合を最適化した手法について詳しく解説します。 課題:モダンアプリケーションにおけるSQLパフォーマンス 従来のSQLデータベースは一貫性や複雑なリレーションのモデル化に優れていま