Next.jsとTwitter API v2で作る自分専用のX投稿アプリ【第2部:ツイート表示編】自分のツイートを画面に表示しよう
第2部へようこそ!
第1部で「Xでログイン」ボタンが動くようになりましたね。おめでとうございます!
でも、「ログインできただけで、何も表示されない...」と思っていませんか?
安心してください。この第2部では、自分のツイートを画面に表示します。
この第2部のゴール
ログイン後、自分の最新ツイート5件が画面に表示される
たったこれだけです。しかし、ここまでできれば、第3部のツイート投稿・削除は簡単になります。
3部作の進捗状況
- ✅ 第1部(完了): ログイン機能
- 🔄 第2部(今回): ツイート表示機能
- ⏳ 第3部(次回): ツイート投稿・削除機能
目次
- Twitter APIからデータを取得する仕組み
- 型定義を作成する
- APIルートを作成する
- クライアント側APIクライアントを作成する
- UIコンポーネントを作成する
- 動作確認
- よくあるエラーと解決方法
1. Twitter APIからデータを取得する仕組み
1-1. なぜ直接Twitter APIを呼べないのか?
第1部で取得した「アクセストークン」があれば、Twitter APIを呼べます。
しかし、ブラウザから直接Twitter APIを呼ぶのは危険です。なぜなら:
❌ アクセストークンがブラウザに公開される
❌ 悪意のあるユーザーがトークンを盗める
❌ 不正にツイートを投稿されるかもしれない
そこで、以下のような流れにします:
1. ブラウザ → 自分のAPIサーバー: 「ツイートが欲しい」
2. 自分のAPIサーバー → Twitter API: アクセストークンを使ってリクエスト
3. Twitter API → 自分のAPIサーバー: ツイートデータを返す
4. 自分のAPIサーバー → ブラウザ: ツイートデータを返す
重要なポイント:
- アクセストークンはサーバー側でのみ使用
- ブラウザにはトークンを公開しない
- セキュリティが保たれる
1-2. Next.jsのAPIルートとは?
Next.jsには、APIルートという機能があります。
app/api/フォルダの中にroute.tsファイルを作ると、自動的にAPIエンドポイントになります。
例:
app/api/twitter/tweets/route.ts
→ http://localhost:3000/api/twitter/tweets
このAPIルートを使って、Twitter APIを呼び出します。
2. 型定義を作成する
TypeScriptを使っているので、まずはツイートのデータ構造を定義します。
2-1. 型定義ファイルを作成
types/tweet.tsを作成します:
touch types/tweet.ts
Windowsの場合:
エクスプローラーでtypesフォルダ内にtweet.tsファイルを作成してください。
2-2. ツイートの型を定義
types/tweet.ts:
// ツイートの型定義
export interface Tweet {
id: string;
text: string;
created_at: string;
author_id: string;
public_metrics: {
retweet_count: number;
reply_count: number;
like_count: number;
quote_count: number;
};
}
export interface TwitterUser {
id: string;
name: string;
username: string;
profile_image_url?: string;
}
export interface TwitterAPIResponse {
data: Tweet[];
includes?: {
users?: TwitterUser[];
};
meta?: {
result_count: number;
newest_id?: string;
oldest_id?: string;
};
}
これは何をしているのか?
Twitter APIから返されるデータの構造を定義しています。
| フィールド | 意味 |
|---|---|
id | ツイートのID |
text | ツイートの本文 |
created_at | 投稿日時 |
author_id | 投稿者のID |
public_metrics | いいね数、リツイート数など |
なぜ型定義が必要?
✅ タイプミスを防げる
✅ IDEの自動補完が効く
✅ データの構造が一目でわかる
3. APIルートを作成する
3-1. ユーザー情報取得APIを作る
まず、自分のユーザーIDを取得するAPIを作ります。
フォルダを作成:
mkdir -p app/api/twitter/me
app/api/twitter/me/route.ts:
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
const TWITTER_API_BASE = "https://api.twitter.com/2";
export async function GET(request: NextRequest) {
try {
// セッションを取得
const session = await getServerSession(authOptions);
// ログインしているか確認
if (!session?.accessToken) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}
// Twitter APIを呼び出す
const url = `${TWITTER_API_BASE}/users/me`;
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
});
// エラー処理
if (!response.ok) {
const error = await response.json();
console.error("Twitter API error:", error);
return NextResponse.json(
{ error: error.detail || "Failed to fetch user info" },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error("Error in /api/twitter/me:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
コードの説明:
セッションの取得
const session = await getServerSession(authOptions);
第1部で保存したアクセストークンを取得します。
Twitter APIを呼び出す
const url = `${TWITTER_API_BASE}/users/me`;
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
});
重要なポイント:
Authorizationヘッダーにアクセストークンを含めるBearerという形式で送る(OAuth 2.0の標準)
エラー処理
if (!response.ok) {
const error = await response.json();
console.error("Twitter API error:", error);
return NextResponse.json(
{ error: error.detail || "Failed to fetch user info" },
{ status: response.status }
);
}
Twitter APIからエラーが返された場合、クライアントにエラーメッセージを返します。
3-2. ツイート一覧取得APIを作る
次に、自分のツイート一覧を取得するAPIを作ります。
フォルダを作成:
mkdir -p app/api/twitter/tweets
app/api/twitter/tweets/route.ts:
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
const TWITTER_API_BASE = "https://api.twitter.com/2";
export async function GET(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.accessToken) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}
// クエリパラメータを取得
const { searchParams } = new URL(request.url);
const userId = searchParams.get("userId");
const maxResults = searchParams.get("maxResults") || "5";
if (!userId) {
return NextResponse.json(
{ error: "userId is required" },
{ status: 400 }
);
}
// Twitter APIを呼び出す
const url = `${TWITTER_API_BASE}/users/${userId}/tweets?max_results=${maxResults}&tweet.fields=created_at,public_metrics,author_id`;
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${session.accessToken}`,
"Content-Type": "application/json",
},
});
// レート制限のチェック
if (response.status === 429) {
const rateLimitReset = response.headers.get('x-rate-limit-reset');
const resetTime = rateLimitReset ? parseInt(rateLimitReset) : Math.floor(Date.now() / 1000) + 900;
const waitSeconds = Math.max(0, resetTime - Math.floor(Date.now() / 1000));
const waitMinutes = Math.ceil(waitSeconds / 60);
return NextResponse.json(
{
error: `API制限に達しました。約${waitMinutes}分後に再度お試しください。`,
rateLimitExceeded: true,
retryAfter: waitSeconds,
},
{ status: 429 }
);
}
if (!response.ok) {
const error = await response.json();
return NextResponse.json(
{ error: error.detail || "Failed to fetch tweets" },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error("Error in /api/twitter/tweets:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
コードの説明:
クエリパラメータの取得
const { searchParams } = new URL(request.url);
const userId = searchParams.get("userId");
const maxResults = searchParams.get("maxResults") || "5";
URLからuserIdとmaxResultsを取得します。
例:/api/twitter/tweets?userId=123&maxResults=5
Twitter APIのパラメータ
const url = `${TWITTER_API_BASE}/users/${userId}/tweets?max_results=${maxResults}&tweet.fields=created_at,public_metrics,author_id`;
パラメータの意味:
| パラメータ | 意味 |
|---|---|
max_results | 取得するツイート数(最大100) |
tweet.fields | 取得するフィールドを指定 |
なぜtweet.fieldsが必要?
デフォルトでは、Twitter APIはidとtextしか返しません。投稿日時やいいね数を取得するには、tweet.fieldsで明示的に指定する必要があります。
レート制限への対応
if (response.status === 429) {
// レート制限に達した場合の処理
}
Twitter API Free Tierには制限があります:
- ツイート読み取り:月10,000回
429エラーが返された場合、待ち時間を計算してユーザーに通知します。
4. クライアント側APIクライアントを作成する
4-1. APIクライアントファイルを作成
lib/twitter-api.ts:
// Twitter APIクライアント
import { Tweet, TwitterAPIResponse } from '@/types/tweet';
/**
* ユーザー情報を取得
*/
export async function getMe(): Promise<{ id: string; username: string; name: string }> {
try {
const response = await fetch('/api/twitter/me');
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to fetch user info');
}
const data = await response.json();
return data.data;
} catch (error) {
console.error('Error fetching user info:', error);
throw error;
}
}
/**
* ユーザーのツイートを取得
*/
export async function getUserTweets(
userId: string,
maxResults: number = 5
): Promise<Tweet[]> {
try {
const url = `/api/twitter/tweets?userId=${userId}&maxResults=${maxResults}`;
const response = await fetch(url);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to fetch tweets');
}
const data: TwitterAPIResponse = await response.json();
return data.data || [];
} catch (error) {
console.error('Error fetching tweets:', error);
throw error;
}
}
これは何をしているのか?
ブラウザから自分のAPIルートを呼び出す関数を定義しています。
なぜ別ファイルにするのか?
✅ コードの再利用ができる
✅ エラーハンドリングを一箇所にまとめられる
✅ 複数のコンポーネントから使える
5. UIコンポーネントを作成する
5-1. ユーティリティ関数を作成
日付をフォーマットする関数を作ります。
lib/utils.ts:
/**
* 日付を「◯分前」「◯時間前」形式にフォーマット
*/
export function formatDate(dateString: string): string {
const date = new Date(dateString);
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (diffInSeconds < 60) {
return `${diffInSeconds}秒前`;
}
const diffInMinutes = Math.floor(diffInSeconds / 60);
if (diffInMinutes < 60) {
return `${diffInMinutes}分前`;
}
const diffInHours = Math.floor(diffInMinutes / 60);
if (diffInHours < 24) {
return `${diffInHours}時間前`;
}
const diffInDays = Math.floor(diffInHours / 24);
if (diffInDays < 7) {
return `${diffInDays}日前`;
}
return date.toLocaleDateString('ja-JP', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
これは何をしているのか?
Twitterのように「3分前」「2時間前」という形式で日付を表示します。
5-2. ツイート1件を表示するコンポーネント
components/フォルダを作成:
mkdir components
components/TweetItem.tsx:
"use client";
import { Tweet } from "@/types/tweet";
import { formatDate } from "@/lib/utils";
interface TweetItemProps {
tweet: Tweet;
username: string;
}
export default function TweetItem({ tweet, username }: TweetItemProps) {
return (
<div className="border-b border-gray-200 p-4 hover:bg-gray-50 transition-colors">
<div className="flex items-start gap-3">
{/* アイコン */}
<div className="w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center text-white font-bold">
{username.charAt(0).toUpperCase()}
</div>
{/* ツイート本文 */}
<div className="flex-1 min-w-0">
{/* ユーザー名と日時 */}
<div className="flex items-center gap-2 mb-1">
<span className="font-semibold text-gray-900">
@{username}
</span>
<span className="text-sm text-gray-500">
{formatDate(tweet.created_at)}
</span>
</div>
{/* ツイート本文 */}
<p className="text-gray-900 whitespace-pre-wrap break-words">
{tweet.text}
</p>
{/* いいね数、リツイート数 */}
<div className="flex items-center gap-4 mt-2 text-sm text-gray-500">
<span>💬 {tweet.public_metrics.reply_count}</span>
<span>🔁 {tweet.public_metrics.retweet_count}</span>
<span>❤️ {tweet.public_metrics.like_count}</span>
</div>
</div>
</div>
</div>
);
}
コードの説明:
- アイコン:ユーザー名の最初の文字を表示(プロフィール画像の代わり)
- 日時:
formatDate関数で「◯分前」形式に変換 - 本文:
whitespace-pre-wrapで改行を保持 - メトリクス:リプライ数、リツイート数、いいね数を表示
5-3. ツイート一覧を表示するコンポーネント
components/TweetList.tsx:
"use client";
import { Tweet } from "@/types/tweet";
import TweetItem from "./TweetItem";
interface TweetListProps {
tweets: Tweet[];
username: string;
isLoading: boolean;
isRefreshing: boolean;
onRefresh: () => void;
}
export default function TweetList({
tweets,
username,
isLoading,
isRefreshing,
onRefresh,
}: TweetListProps) {
if (isLoading) {
return (
<div className="bg-white border border-gray-200 rounded-lg p-6">
<p className="text-center text-gray-500">読み込み中...</p>
</div>
);
}
return (
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
{/* ヘッダー */}
<div className="border-b border-gray-200 p-4 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">
最近の投稿
</h2>
<button
onClick={onRefresh}
disabled={isRefreshing}
className="text-sm text-blue-500 hover:text-blue-600 disabled:opacity-50"
>
{isRefreshing ? "更新中..." : "🔄 更新"}
</button>
</div>
{/* ツイート一覧 */}
{tweets.length === 0 ? (
<div className="p-6 text-center text-gray-500">
まだツイートがありません
</div>
) : (
<div>
{tweets.map((tweet) => (
<TweetItem
key={tweet.id}
tweet={tweet}
username={username}
/>
))}
</div>
)}
</div>
);
}
コードの説明:
- ローディング状態:
isLoadingがtrueの間は「読み込み中...」を表示 - 更新ボタン:クリックすると
onRefresh関数が呼ばれる - 空の状態:ツイートがない場合は「まだツイートがありません」を表示
- 一覧表示:
map関数で各ツイートをTweetItemコンポーネントで表示
5.4. メインページを更新
app/page.tsxを更新して、ツイートを表示します:
"use client";
import { useSession, signIn, signOut } from "next-auth/react";
import { useState, useEffect, useCallback } from "react";
import { Tweet } from "@/types/tweet";
import { getUserTweets, getMe } from "@/lib/twitter-api";
import TweetList from "@/components/TweetList";
export default function Home() {
const { data: session, status } = useSession();
const [tweets, setTweets] = useState<Tweet[]>([]);
const [userInfo, setUserInfo] = useState<{ id: string; username: string; name: string } | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
// ユーザー情報を取得
const fetchUserInfo = useCallback(async () => {
try {
const info = await getMe();
setUserInfo(info);
return info;
} catch (err) {
console.error("Failed to fetch user info:", err);
setError("ユーザー情報の取得に失敗しました");
return null;
}
}, []);
// ツイートを取得
const fetchTweets = useCallback(async (userId: string) => {
try {
const fetchedTweets = await getUserTweets(userId, 5);
setTweets(fetchedTweets);
setError(null);
} catch (err) {
console.error("Failed to fetch tweets:", err);
setError("投稿の取得に失敗しました");
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
}, []);
// 初回ロード
useEffect(() => {
if (session) {
const loadData = async () => {
const info = await fetchUserInfo();
if (info) {
await fetchTweets(info.id);
}
};
loadData();
} else {
setIsLoading(false);
}
}, [session, fetchUserInfo, fetchTweets]);
// ツイートを更新
const handleRefresh = async () => {
if (!session || !userInfo) return;
setIsRefreshing(true);
await fetchTweets(userInfo.id);
};
if (status === "loading") {
return (
<div className="flex min-h-screen items-center justify-center">
<p className="text-gray-500">読み込み中...</p>
</div>
);
}
if (!session) {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50">
<div className="space-y-6 text-center">
<div className="space-y-2">
<h1 className="text-4xl font-bold text-gray-900">X My Posts</h1>
<p className="text-gray-600">
自分の投稿に集中できるシンプルなXクライアント
</p>
</div>
<button
onClick={() => signIn("twitter")}
className="px-6 py-3 bg-blue-500 text-white font-medium rounded-full hover:bg-blue-600 transition-colors"
>
X (Twitter) でサインイン
</button>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
{/* ヘッダー */}
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
<div className="max-w-2xl mx-auto px-4 py-4 flex items-center justify-between">
<h1 className="text-xl font-bold text-gray-900">X My Posts</h1>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600">
@{userInfo?.username || session.user?.name}
</span>
<button
onClick={() => signOut()}
className="text-sm text-gray-600 hover:text-gray-900"
>
サインアウト
</button>
</div>
</div>
</header>
{/* メインコンテンツ */}
<main className="max-w-2xl mx-auto px-4 py-6 space-y-6">
{/* エラーメッセージ */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-600 text-sm">{error}</p>
</div>
)}
{/* ツイート一覧 */}
<TweetList
tweets={tweets}
username={userInfo?.username || ""}
isLoading={isLoading}
isRefreshing={isRefreshing}
onRefresh={handleRefresh}
/>
</main>
</div>
);
}
コードの説明:
状態管理
const [tweets, setTweets] = useState<Tweet[]>([]);
const [userInfo, setUserInfo] = useState<...>(null);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
各状態の意味:
| 状態 | 意味 |
|---|---|
tweets | 取得したツイート一覧 |
userInfo | ユーザー情報(ID、ユーザー名) |
isLoading | 初回ロード中か |
isRefreshing | 更新ボタンを押した後か |
error | エラーメッセージ |
データ取得の流れ
useEffect(() => {
if (session) {
const loadData = async () => {
const info = await fetchUserInfo(); // 1. ユーザー情報を取得
if (info) {
await fetchTweets(info.id); // 2. ツイートを取得
}
};
loadData();
}
}, [session, fetchUserInfo, fetchTweets]);
流れ:
- ログイン後、
useEffectが実行される fetchUserInfoでユーザーIDを取得- そのIDを使って
fetchTweetsでツイートを取得
6. 動作確認
6-1. 開発サーバーを起動
ターミナルで以下を実行します:
npm run dev
ブラウザでhttp://localhost:3000を開きます。
6-2. ログインしてツイートを確認
手順:
- 「X (Twitter) でサインイン」ボタンをクリック
- Twitter認証を完了
- 自分の最新ツイート5件が表示される!
成功です!
6-3. 更新ボタンを試す
「🔄 更新」ボタンをクリックすると、最新のツイートが再取得されます。
7. よくあるエラーと解決方法
エラー1: 「Failed to fetch user info」
症状:
ユーザー情報が取得できず、エラーメッセージが表示される。
原因:
- アクセストークンが期限切れ
- Twitter APIのレート制限に達した
- ネットワークエラー
解決方法:
- サインアウトして再サインイン
- 少し待ってから再度試す
- ブラウザの開発者ツールでネットワークエラーを確認
エラー2: 「Failed to fetch tweets」
症状:
ツイート一覧が表示されず、エラーメッセージが表示される。
原因:
- userIdが正しく渡されていない
- Twitter APIのレート制限
- ツイートが0件
解決方法:
開発者ツールでデバッグ:
- F12キーを押して開発者ツールを開く
- Consoleタブでエラーログを確認
- NetworkタブでAPIリクエストを確認
よくあるパターン:
Failed to fetch tweets: userId is required
→ ユーザー情報の取得に失敗している可能性
エラー3: 「API制限に達しました」
症状:
「約15分後に再度お試しください」というメッセージが表示される。
原因:
Twitter API Free Tierのレート制限に達した。
解決方法:
✅ 15分待つ
✅ リロードを繰り返さない
✅ 不要なAPI呼び出しを減らす
レート制限の詳細:
| エンドポイント | 制限 |
|---|---|
| ユーザー情報取得 | 15分あたり900回 |
| ツイート取得 | 15分あたり900回 |
開発中は問題になりませんが、本番環境では注意が必要です。
エラー4: ツイートが表示されない
症状:
エラーはないが、ツイートが0件と表示される。
原因:
- まだツイートを投稿していない
- APIが古いツイートしか取得できていない
解決方法:
- Twitterアプリで新しいツイートを投稿
- 「🔄 更新」ボタンをクリック
- それでも表示されない場合、開発者ツールでレスポンスを確認
まとめ
この記事でできたこと
✅ Twitter APIからデータを取得する仕組みを理解した
✅ APIルートを作成した(ユーザー情報、ツイート一覧)
✅ クライアント側APIクライアントを作成した
✅ UIコンポーネントを作成した(ツイート表示)
✅ 自分の最新ツイート5件が画面に表示された!
次の第3部では
第3部では、ツイート投稿・削除機能を実装します。
内容:
- ツイート投稿APIの実装
- ツイート削除APIの実装
- ツイート作成コンポーネント
- リアルタイムプレビュー機能
- よくあるエラー(403 Forbidden)と解決方法
第2部でツイート表示ができているので、第3部はもっと楽しいです!
参考資料
公式ドキュメント
- Twitter API v2 - User Lookup - ユーザー情報取得
- Twitter API v2 - Tweets Lookup - ツイート取得
- Next.js API Routes - APIルートの作り方
Twitter API関連
- Twitter APIのレート制限について - レート制限の詳細
- Twitter API v2のフィールド指定 - tweet.fieldsの使い方
React Hooks関連
- useEffect完全ガイド - useEffectの使い方
- useCallbackの使い方 - useCallbackの公式ドキュメント
エラーハンドリング
- Next.jsでのエラーハンドリング - エラー処理のベストプラクティス
- fetch APIの使い方 - fetchの基礎
お疲れさまでした!🎉
次の第3部で、ついに完成します!