最もミニマルなNext.js TODOアプリの作り方
この記事では、ServerlessなNext.jsをベースにしたTODOアプリケーションを構築します。可能な限りミニマルにすることを目指します。データベース接続は一切使わず、Next.js以外の余計な依存関係も持ちません。ボタンすらありません。ミニマリズムはクールで洗練されていて素晴らしいのですが、私がこれを愛する最大の理由は、単純に怠惰な開発者だからこそ実装の手間が少ないことに魅力を感じるからです:)
なぜデータベース接続を避けるのか?
Next.jsは、フロントエンド開発者がフルスタックアプリケーションを開発できるようにするモダンなフレームワークです。サーバーレス関数は、Next.js開発者にとってバックエンド開発を大幅にシンプルにする重要な役割を担っています。しかしご存知の通り、サーバーレス関数はステートレスな性質を持つため、データベース接続とは相性が良くありません。サーバーレス関数内でのデータベース接続が引き起こす問題の具体例については、こちらやこちらを参照してください。
RESTがその答え
RESTは、セッション情報を持たずにクライアントとサーバーが通信できる仕組みです。このステートレス性とシンプルな設計により、RESTはサーバーレス環境に最適な通信プロトコルと言えます。本記事では、Upstash RedisにREST API経由でアクセスします。
プロジェクトのスタック
- フロントエンド:Next.js
- バックエンド:Vercel関数
- データベース:Upstash Redis(REST API使用)
デモはこちら:https://nextjs-todo-zeta.vercel.app/
ソースコードはこちら:https://github.com/upstash/examples/tree/master/nextjs-todo
プロジェクトのセットアップ
まず、次のコマンドでNext.jsアプリを作成します:npx create-next-app
次に、AWS-US-EAST-1リージョンにUpstash Redisデータベースを作成し、REST URLとトークンをコピーしておきます。
このプロジェクトは、以下の3つのAPIエンドポイントを持つシングルページアプリケーションになります:
- pages/api/list.js:TODOアイテムの一覧を取得
- pages/api/add.js:TODOアイテムを追加
- pages/api/remove.js:TODOアイテムを削除
コード
pages/api/list.js を以下のように作成します:
export default async (req, res) => {
const token = "REPLACE_YOUR_TOKEN";
const url = "https://REPLACE_YOUR_ENDPOINT/lrange/todo/0/100?_token=" + token;
return fetch(url)
.then((r) => r.json())
.then((data) => {
let result = JSON.stringify(data.result);
return res.status(200).json(result);
});
};
pages/api/add.js を以下のように作成します:
export default async (req, res) => {
if (!req.query.todo) {
return res.status(400).send("todo parameter required.");
}
let todo = encodeURI(req.query.todo);
const token = "REPLACE_YOUR_TOKEN";
const url =
"https://REPLACE_YOUR_ENDPOINT/lpush/todo/" + todo + "?_token=" + token;
return fetch(url)
.then((r) => r.json())
.then((data) => {
let result = JSON.stringify(data.result);
return res.status(200).json(result);
});
};
pages/api/remove.js を以下のように作成します:
export default async (req, res) => {
if (!req.query.todo) {
return res.status(400).send("todo parameter required.");
}
let todo = encodeURI(req.query.todo);
const token = "REPLACE_YOUR_TOKEN";
const url =
"https://REPLACE_YOUR_ENDPOINT/lrem/todo/1/" + todo + "?_token=" + token;
return fetch(url)
.then((r) => r.json())
.then((data) => {
let result = JSON.stringify(data.result);
return res.status(200).json(result);
});
};
次に、pages/index.js を以下のように更新します:
import Head from "next/head";
import Image from "next/image";
import styles from "../styles/Home.module.css";
import { useEffect, useState } from "react";
export default function Home() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [todo, setTodo] = useState("");
let changeHandler = (event) => {
setTodo(event.target.value);
};
let addTodo = (event) => {
setLoading(true);
event.preventDefault();
fetch("/api/add?todo=" + todo)
.then((res) => res.json())
.then((data) => {
loadTodos();
});
};
let removeTodo = (rtodo) => {
setLoading(true);
fetch("/api/remove?todo=" + rtodo)
.then((res) => res.json())
.then((data) => {
loadTodos();
});
};
let loadTodos = () => {
console.log("load todos");
fetch("/api/list")
.then((res) => res.json())
.then((data) => {
setData(data);
setLoading(false);
});
};
useEffect(() => {
setLoading(true);
loadTodos();
}, []);
if (!data) return "Loading...";
return (
<div className={styles.container}>
<Head>
<title>Next.js TODO APP</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<div className={styles.grid}>
<h1 className={styles.title}>
TODO App with{" "}
<a href="https://blog.upstash.com/nextjs-todo">Next.js!</a>
<br />
<br />
</h1>
{loading ? (
<a href="#" className={styles.card}>
<img src="/loader.gif" />
</a>
) : (
<form className={styles.cardForm} onSubmit={addTodo}>
<input
className={styles.cardInput}
type="text"
name="todo"
onChange={changeHandler}
placeholder="Enter your exciting TODO item!"
/>
</form>
)}
{data.map((item) => (
<a
href="#"
onClick={() => removeTodo(item)}
className={styles.card}
>
<p>{item}</p>
</a>
))}
</div>
</main>
<footer className={styles.footer}>
<a
href="https://blog.upstash.com/nextjs-todo"
target="_blank"
rel="noopener noreferrer"
>
Powered by{" "}
<span className={styles.logo}>
<Image src="/logo.png" alt="Upstash Logo" width={87} height={25} />
</span>
</a>
</footer>
</div>
);
}
ご覧の通り、これはReactフックを使用したごく基本的なアプリケーションです。APIとやり取りするメソッドは、addTodo、removeTodo、loadTodos の3つだけです。
最後に、styles/Home.module.css ファイルをこちらのように更新すれば完了です。
実行とデプロイ
npm run dev コマンドでプロジェクトをローカル環境で実行します。動作に問題がなければ、プロジェクトフォルダ内で vercel コマンドを実行するだけでデプロイできます。VercelがAPI関数に対応するサーバーレス関数を自動的に作成してくれます。Vercel関数のデフォルトリージョンはUS-EAST-1のため、データベースも同じリージョンに作成しています。
注意点
- データベースのトークンは、Vercelの環境変数として管理する方が安全です。
- パフォーマンスを最大化するには、サーバーレス関数とRedisデータベースを同じリージョンに配置するのがベストです。
- REST APIの代わりにRedisクライアントを使うことも可能ですが、前述の通り、サーバーレス関数内でのデータベース接続は問題を引き起こす可能性があります。なお、実際に試したところ、Upstash REST APIとネイティブAPIの間に大きなパフォーマンス差は確認できませんでした。
-
Googleニュースアプリを最大限に活用するための5つのヒントとコツ
Googleは開発者向けイベント「Google I/O」にて、専用のニュースアプリ「Googleニュース」を発表しました。このアプリは、従来の「Googleニュース&天気」と「ニューススタンド(Newsstand)」の機能を統合したものであり、両アプリの役割を実質的に置き換える存在となっています。ユーザーの興味や好みに基づいて世界中のニュースを自動的に配信し、快適なニュース閲覧体験を提供してくれるのが魅力です。 ここでは、Googleニュースアプリの主な機能をご紹介します。しかしそれだけではありません。まだまだ知られていない便利な機能が数多く搭載されています。本記事では、お気に入りのニュース
-
YouTube Musicの便利な機能5選!知っておきたい活用テクニック
オンラインストリーミングは、今や私たちの生活に欠かせない存在となっています。テレビでお気に入りの映画やドラマを視聴したり、スマートフォンで音楽を楽しんだりと、スマートデバイスでできる娯楽の中でも、ストリーミングは最も人気のある選択肢の一つです。最大のメリットは、端末のストレージ容量を気にする必要がないこと。大量のコンテンツをダウンロードせずに探索でき、いつでも手軽にアクセスできる点が魅力です。 音楽ストリーミングサービスといえば、真っ先にSpotifyを思い浮かべる方が多いでしょう。しかし、Spotifyに匹敵する強力なライバルが登場しました。それがGoogleが手掛ける「YouTube Mu