Blitz.jsとUpstash Redisで作るToDoリストアプリ完全ガイド
Blitz.jsは、Next.jsからフォークされたReactフレームワークです。この記事では、タスクをUpstashに保存するToDoアプリケーションをBlitz.jsで構築する方法を、環境構築からフロントエンドの実装まで順を追って解説します。それでは早速始めましょう!
セットアップ
まずは、お使いのコンピュータにBlitz.jsをインストールする必要があります。
NPMの場合:
npm install -g blitz --legacy-peer-deps
Yarnの場合:
yarn global add blitz
新しいBlitz.jsアプリを作成するには、blitz newコマンドを使用し、作成されたディレクトリに移動します。
blitz new blitzjs-todo && cd blitzjs-todo
次に、WebサイトのスタイリングのためにTailwindCSSをインストールしましょう。
blitz install tailwind
最後に、Upstash APIへの呼び出しを簡単にするため、Upstash JS SDKをインストールします。
NPMの場合:
npm i @upstash/redis
Yarnの場合:
yarn i @upstash/redis
ここまで完了したら、blitz devを実行して、すべてが正しく動作していることを確認してください。アカウントを作成してサインインも試してみてください。ここまでの手順が正しく行えていれば、以下のような画面が表示されるはずです。

また、ファイル構成は以下のようになっているはずです。

UpstashコンソールからUPSTASH_REDIS_REST_URLとUPSTASH_REDIS_REST_TOKENをコピーし、.envファイルに貼り付けてください。ファイルは以下のようになります。
# このenvファイルはソース管理にコミットして問題ありません
# すべての環境のデフォルト値を記述する場所です
# `.env.local`と`.env.production`の値がこれらの値を上書きします
UPSTASH_REDIS_REST_URL=YOUR_URL_HERE
UPSTASH_REDIS_REST_TOKEN=YOUR_TOKEN_HERE
これでBlitz.jsアプリケーションのセットアップは完了です!それでは、ToDoリストの実装に取り掛かりましょう。
実装
Blitz.jsにはユーザー認証機能が最初から組み込まれています!これを活用して、ユーザーごとのプライベートなToDoリストを作成しましょう。
まず、/lib/redis.tsでUpstash JS SDKを初期化します。
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
export default redis;
ToDoリストにアクセスするために、3つの異なるAPIルートを作成する必要があります。
全件取得ルート(getall)
app/apiディレクトリに移動し、getall.tsというファイルを作成します。作成したら、以下のコードを貼り付けてください。
import { BlitzApiRequest, BlitzApiResponse, getSession } from "blitz";
import redis from "../../lib/redis";
export const handler = async (req: BlitzApiRequest, res: BlitzApiResponse) => {
const session = await getSession(req, res);
if (!session.userId) {
res.status(401).json({ error: `Do not tamper with this route!` });
} else {
await redis
.lrange(String(session.userId), 0, 100)
.then((data) => res.status(200).json({ data: data, success: true }))
.catch((error) => res.status(500).json({ error: error }));
}
};
export default handler;
このAPIルートの仕組みを、ステップごとに見ていきましょう。まず、ルートへのリクエストを受け取ります。ルート内では、ユーザーがログイン済みかどうかを検証します。ユーザーが存在しない場合は「Not Authorized(未承認)」のレスポンスを返します。ユーザーが存在する場合は、Upstash Redisデータベースにアクセスし、リスト内の現在のToDoをすべて取得します。これにより、最大100件程度のToDoが取得されます。
Q:「そもそも、どうやってToDoを追加するんですか?」
A:いい質問ですね!次はその追加機能を実装しましょう!
追加ルート(add)
app/apiディレクトリにadd.tsという新規ファイルを作成し、以下のコードを貼り付けてください。
import { BlitzApiRequest, BlitzApiResponse, getSession } from "blitz";
import redis from "../../lib/redis";
const handler = async (req: BlitzApiRequest, res: BlitzApiResponse) => {
const session = await getSession(req, res);
if (req.method !== "POST" || !req.body.data || !session.userId) {
res.status(401).json({ error: `Do not tamper with this route!` });
} else {
let todo = encodeURI(req.body.data);
await redis
.lpush(String(session.userId), todo)
.then(() => res.status(200).json({ success: true }))
.catch(() => res.status(500).json({ error: "Error adding data." }));
}
};
export default handler;
このAPIルートは前のものとよく似ていますが、5行目にチェックが追加されている点に注目してください。これは、このリクエストがGETリクエストではなくPOSTリクエストであるためです。3つの項目をチェックしていることに注意してください。まず、リクエストが実際にPOSTリクエストであることを確認します。次に、req.body.dataにJSONまたはテキストが含まれていることを確認します。最後に、ユーザーがログインしていることを確認します。これらのチェックをすべて通過すれば、ToDoをUpstash上のRedisリストにプッシュできます。処理中に何らかのエラーが発生した場合は、.catchを使って500エラーを返します。
削除ルート(remove)
最後に必要なルートは、ToDoを削除するためのものです。タスクを完了したら、もちろんリストから消去しますよね!app/api/remove.tsに最後のAPIルートを追加しましょう。以下のコードをファイルにコピーしてください。
import { BlitzApiRequest, BlitzApiResponse, getSession } from "blitz";
import redis from "../../lib/redis";
const handler = async (req: BlitzApiRequest, res: BlitzApiResponse) => {
const session = await getSession(req, res);
if (req.method !== "POST" || !req.body.data || !session.userId) {
res.status(401).json({ error: `Do not tamper with this route!` });
} else {
let todo = encodeURI(req.body.data);
await redis
.lrem(String(session.userId), 1, todo)
.then(() => res.status(200).json({ success: true }))
.catch(() => res.status(500).json({ error: "Error removing data." }));
}
};
export default handler;
何か似ていることに気づきましたか?そう、このルートはaddAPIルートとほぼ同一だからです。大きな違いは、Redisからアイテムを削除するためにLPUSHではなくLREMを使用している点です。
フロントエンドの構築
まずは、app/pages/index.jsの中身をすべて削除し、ToDoリストを一歩一歩書いていきましょう。
ファイルの先頭に、以下のインポート文を貼り付けます。
import { Link, BlitzPage, useMutation, Routes, getAntiCSRFToken } from "blitz";
import { useRef, useEffect, useState, Suspense } from "react";
import Layout from "app/core/layouts/Layout";
import { useCurrentUser } from "app/core/hooks/useCurrentUser";
import logout from "app/auth/mutations/logout";
ToDoリストのコア機能の構築にはReact Hooksを使用します。リストの主要な機能を実装していきましょう。
const Main = () => {
const todoRef = useRef<HTMLInputElement>(null)
const [todos, setTodos] = useState([])
const currentUser = useCurrentUser()
const [logoutMutation] = useMutation(logout)
const handleAddTodo = async (e) => {
e.preventDefault()
const antiCSRFToken = await getAntiCSRFToken()
const response = await fetch("/api/add", {
method: "POST",
headers: {
"Content-Type": "application/json",
"anti-csrf": antiCSRFToken,
},
body: JSON.stringify({ data: todoRef.current?.value }),
})
const data = await response.json()
if (data.success) {
todoRef.current!.value = ""
fetchTodos()
}
}
const handleRemoveTodo = async (id) => {
const antiCSRFToken = await getAntiCSRFToken()
const response = await fetch("/api/remove", {
method: "POST",
headers: {
"Content-Type": "application/json",
"anti-csrf": antiCSRFToken,
},
body: JSON.stringify({ data: id }),
})
const data = await response.json()
if (data.success) {
fetchTodos()
}
}
const fetchTodos = async () => {
const antiCSRFToken = await getAntiCSRFToken()
const response = await fetch("/api/getall", {
method: "GET",
headers: {
"anti-csrf": antiCSRFToken,
},
})
const res = await response.json()
setTodos(res.data)
}
useEffect(() => {
fetchTodos()
}, [])
if (currentUser) {
return (
<>
<button
className="mt-4 px-2 py-1 border-2 border-black hover:bg-gray-400 mb-3"
onClick={async () => {
await logoutMutation()
}}
>
Logout
</button>
<div>
User id: <code>{currentUser.id}</code>
<br />
User email: <code>{currentUser.email}</code>
</div>
<form className="mt-2" onSubmit={handleAddTodo}>
<p>add a todo:</p>
<input
ref={todoRef}
className="w-full border-black border-2 focus:outline-none text-center"
/>
</form>
<div className="flex flex-col gap-2 mt-4 bg-gray-300 rounded-md">
{(todos as string[]).map((todo: string, index: number) => (
<div className="flex items-center p-3 rounded-md bg-gray-300" key={index}>
<button
onClick={() => handleRemoveTodo(todo)}
className="flex items-center mr-4 justify-center w-5 h-5 rounded-[0.25rem] border border-solid border-gray-500 shadow-sm hover:bg-gray-700"
></button>
<span>{todo}</span>
</div>
))}
</div>
</>
)
} else {
return (
<div className="flex flex-col gap-4 text-center">
<Link href={Routes.SignupPage()}>
<a className="mt-4 px-2 py-1 border-2 border-black hover:bg-gray-400">
<strong>Sign Up</strong>
</a>
</Link>
<Link href={Routes.LoginPage()}>
<a className="mt-4 px-2 py-1 border-2 border-black hover:bg-gray-400">
<strong>Login</strong>
</a>
</Link>
</div>
)
}
}
<Main/>コンポーネントは、このアプリケーションの中核となる部分です。コードを詳しく見てみると、その使い方がわかります。コンポーネントの先頭では、アプリケーションの状態(state)を初期化しています。また、後ほど「新規ToDo」入力フィールドで使用するためのrefも宣言しています。antiCSRFTokenの使用にも注目してください!Blitz.jsでは、悪意ある攻撃者からサイトを守るため、APIルートへフェッチする際にこれらのトークンを使用することが必須となっています。セキュリティ対策として用意されているのは嬉しいポイントですね。
Webサイト上のデータ処理には、主に3つの関数を使用します。それは以下の3つです。
handleAddTodo(ToDoの追加)handleRemoveTodo(ToDoの削除)fetchTodos(ToDoの一覧取得)
ページが読み込まれるとすぐにfetchTodosが呼び出され、ユーザーがまだ完了していないToDoがすべて読み込まれます。ユーザーがToDoを追加または削除すると、再度fetchTodosが呼び出され、その変更がWebサイトに反映されます!
ユーザーがログインしていない場合、このページを見る前にログインを促す画面が表示されます。
まだアカウントをお持ちでない場合は、サインアップまたはログインが可能です。アカウントがなければToDoを保存できないこと、そしてすべてのAPIルートがAntiCSRFTokenによる認証を要求することを忘れないでください!
しかし、もう1つ重要なステップが残っています!ページをエクスポートしなければなりません。
const Home: BlitzPage = () => {
return (
<div className="flex flex-col min-h-screen items-center justify-center">
<main>
<div className="my-4">
<Suspense fallback="Loading...">
<Main />
</Suspense>
</div>
</main>
</div>
);
};
Home.suppressFirstRenderFlicker = true;
Home.getLayout = (page) => <Layout title="Home">{page}</Layout>;
export default Home;
上記のように、Blitz.jsはNext.jsとは少し異なるアプローチを採用していますが、現時点では本質的には同じ考え方に基づいています。先ほどインポートしたSuspenseを使用して、アプリが読み込み中であることをユーザーに表示し、読み込みが完了した後に<Main/>コンポーネントを表示します!
変更内容を確認するには、もう一度コンソールで以下のコマンドを実行し、ブラウザでアプリにアクセスしてください。
blitz dev
手順に従って進めていれば、ログインしていくつかのToDoを追加した後、アプリケーションは以下のような見た目になっているはずです。

ToDoの横にあるボックスをクリックすると、そのToDoを削除できます。これこそがremoveTodo関数の役割です😉。
お疲れさまでした!
この記事を読んで何か新しいことを学べたのであれば幸いです。もし既に知っている内容だったとしても、スキルをブラッシュアップするのに損はありません!Blitz.jsはNext.jsからの方針転換を進めているため、将来的にはまったく別のフレームワークになる可能性もあります。最新情報は公式サイトでチェックしてみてください!
プロジェクトのソースコード: GitHubリンク
動作デモ: デモリンク
フィードバックがある方は、Twitterで@upstashをフォローし、Discordサーバーにぜひ参加してください!
-
エッジキャッシングで実現する、世界どこでも5ミリ秒のRedisレイテンシ
Redisでは、データベースとクライアントが同じリージョン内にあれば、1ミリ秒のレイテンシは容易に実現できます。しかし、クライアントが世界中に分散している場合、レイテンシは100ミリ秒を超えてしまいます。この課題を解決するために開発されたのが「Edge Caching(エッジキャッシング)」です。 エッジキャッシングとは エッジキャッシングでは、CDNと同じようにREST APIのレスポンスが世界各地のエッジロケーションにキャッシュされます。エッジキャッシングを有効にすると、平均5ミリ秒というグローバルレイテンシを実現できます。実際に、10の異なるリージョンに配置したクライアントからレイテンシ
-
Cloudflare WorkersとRedisで実現するエッジコンピューティング活用術
エッジコンピューティングは、近年もっとも注目されている技術のひとつです。CDNがファイルをユーザーの近くに配置できるようにしたのと同じように、エッジコンピューティングはアプリケーションそのものをユーザーの近くで実行できるようにします。これにより、開発者はグローバルに分散され、高いパフォーマンスを発揮するアプリケーションを構築できるようになります。 Cloudflare Workersとステートレス性の課題 現在この分野をリードしている製品がCloudflare Workersです。コールドスタートのないサーバーレス実行環境を提供し、Cloudflareのグローバルネットワークを活かすことで、ア