DEV Community

Hamamoto Kazuma
Hamamoto Kazuma

Posted on • Originally published at beachone1155.vercel.app

R2E APIのデモUIを作りました!Next.js + Render でバックエンド連携する実装記

はじめに

以前 Research→Experience API を作りました という記事で、FastAPI + PostgreSQL(pgvector) で「ビジネス課題 → 関連論文探索 → 示唆」を返す API を紹介しました。

今回は、この API を体験できるフロントエンド UI を Next.js(Vercel)ブログの /portfolio/r2e に追加した実装記です。バックエンドは Render の無料枠で動かしています。

完成したもの

  • URL: /portfolio/r2e
  • 機能: 3タブUI(Search / Sources / Status)
  • バックエンド: Render(https://research-to-experience-api.onrender.com
  • フロントエンド: Next.js(Vercel)

Search タブでビジネス課題を入力すると、Sources タブに論文リストが表示されます。

アーキテクチャ設計

要件

  1. フロントエンドからバックエンド API を叩く
  2. 本番環境では Render、開発環境では localhost:8000 を使い分け
  3. CORS を避けたい(同一オリジン扱いにしたい)

選択肢

最初は Next.js の rewrites を使うことを考えましたが、Vercel 上で POST リクエストが正しくプロキシされない問題が発生しました。最終的に Next.js API Route でバックエンドにプロキシする方式にしました。

// src/app/api/r2e/[...path]/route.ts
export async function GET(request: NextRequest, { params }: { params: { path: string[] } }) {
  return proxyRequest(request, params.path, 'GET')
}

export async function POST(request: NextRequest, { params }: { params: { path: string[] } }) {
  return proxyRequest(request, params.path, 'POST')
}
Enter fullscreen mode Exit fullscreen mode

ディレクトリ構成

external/research-to-experience-api/
  ├── client.ts          # APIクライアント(search, health)
  └── README.md          # 使い方

src/app/
  ├── api/r2e/[...path]/
  │   └── route.ts        # バックエンドプロキシ
  └── portfolio/r2e/
      └── page.tsx        # メインUI
Enter fullscreen mode Exit fullscreen mode

実装詳細

1. API クライアント(薄いラッパー)

external/ フォルダに API クライアントを配置。ドメイン非依存の相対パスで実装しています。

export type SearchResponse = {
  summary?: string[]
  business_implications?: string[]
  sources?: Source[]
}

export async function search(query: string): Promise<SearchResponse> {
  const r = await fetch('/api/r2e/search', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ query }),
  })
  if (!r.ok) {
    const errorData = await r.json().catch(() => ({}))
    if (r.status === 504) {
      throw new Error('バックエンドAPIのコールドスタート中です。30秒ほど待ってから再度お試しください。')
    }
    throw new Error(errorData.details || `HTTP ${r.status}`)
  }
  return r.json()
}
Enter fullscreen mode Exit fullscreen mode

2. API Route(プロキシ実装)

Next.js API Route でバックエンド API にプロキシします。環境変数 BACKEND_URL で開発/本番を切り替えます。

const BACKEND_URL = process.env.BACKEND_URL || 'https://research-to-experience-api.onrender.com'

async function proxyRequest(
  request: NextRequest,
  pathSegments: string[],
  method: 'GET' | 'POST'
) {
  const path = pathSegments.join('/')
  const url = `${BACKEND_URL}/${path}`

  const options: RequestInit = { method, headers: { 'Content-Type': 'application/json' } }

  if (method === 'POST') {
    const body = await request.text()
    options.body = body
  }

  // タイムアウト設定(Render のコールドスタート対応)
  const controller = new AbortController()
  const timeoutId = setTimeout(() => controller.abort(), 90000)

  try {
    const response = await fetch(url, { ...options, signal: controller.signal })
    clearTimeout(timeoutId)
    const data = await response.text()
    const jsonData = JSON.parse(data)
    return NextResponse.json(jsonData, { status: response.status })
  } catch (fetchError) {
    clearTimeout(timeoutId)
    if (fetchError instanceof Error && fetchError.name === 'AbortError') {
      return NextResponse.json(
        { error: 'Request timeout', details: 'Backend API did not respond within 90 seconds...' },
        { status: 504 }
      )
    }
    throw fetchError
  }
}
Enter fullscreen mode Exit fullscreen mode

3. メインUI(3タブ構成)

React の state でタブ管理、API クライアントでデータ取得します。

export default function R2EPage() {
  const [tab, setTab] = useState<Tab>('Search')
  const [query, setQuery] = useState('')
  const [loading, setLoading] = useState(false)
  const [resp, setResp] = useState<SearchResponse | null>(null)

  const doSearch = async () => {
    setLoading(true)
    setError(null)
    try {
      const data = await doApiSearch(query)
      setResp(data)
      setTab('Sources') // 成功時はSourcesへ切替
    } catch (e: unknown) {
      const message = e instanceof Error ? e.message : 'request failed'
      setError(message)
    } finally {
      setLoading(false)
    }
  }

  // ... UI実装
}
Enter fullscreen mode Exit fullscreen mode

タブ構成:

  • Search: ビジネス課題を入力、検索実行
  • Sources: 検索結果の論文リスト(Title, URL, Confidence)
  • Status: /health エンドポイントの疎通確認

環境設定

Vercel 環境変数

Vercel CLI で環境変数を設定しました。

# Production / Preview
echo -n "https://research-to-experience-api.onrender.com" | vercel env add BACKEND_URL production
echo -n "https://research-to-experience-api.onrender.com" | vercel env add BACKEND_URL preview

# Development(ローカル開発用)
echo -n "http://localhost:8000" | vercel env add BACKEND_URL development
Enter fullscreen mode Exit fullscreen mode

Render デプロイ

バックエンドは Render の無料枠でデプロイ。Dockerfile を使っています。

  • URL: https://research-to-experience-api.onrender.com
  • Health Check Path: /health
  • Auto-Deploy: Yes

ハマりポイントと対応

1. POST リクエストが 405 エラー

問題: Next.js の rewrites を使ったが、Vercel 上で POST が正しくプロキシされなかった。

対応: rewrites をやめ、Next.js API Route でプロキシする方式に変更。

// ✗ 最初の実装(rewrites)
// next.config.mjs
async rewrites() {
  return [{ source: '/r2e/:path*', destination: `${BACKEND_URL}/:path*` }]
}

// ✓ 最終実装(API Route)
// src/app/api/r2e/[...path]/route.ts
export async function POST(...) { return proxyRequest(...) }
Enter fullscreen mode Exit fullscreen mode

2. Render のコールドスタート

問題: Render 無料枠は15分間アクセスがないとスリープし、初回アクセスで30〜60秒かかることがある。

対応:

  • タイムアウトを90秒に延長
  • 504エラー時に「コールドスタート中です。30秒ほど待ってから再度お試しください。」と案内
if (r.status === 504) {
  throw new Error('バックエンドAPIのコールドスタート中です。30秒ほど待ってから再度お試しください。')
}
Enter fullscreen mode Exit fullscreen mode

3. ビルド時の環境変数エラー

問題: Vercel ビルド時に BACKEND_URLundefined になり、rewrites の destination が undefined/:path* になってエラー。

対応: API Route ではランタイムで process.env.BACKEND_URL を参照するため、ビルド時の問題は発生しない(rewrites から API Route に変更したので解決)。

学んだこと

  1. Next.js の rewrites は GET には向くが、POST は API Route が確実
  2. 無料ホスティング(Render)のコールドスタートを考慮した UX 設計が重要
  3. 環境変数はビルド時とランタイムで扱いが異なる

まとめ

  • Next.js API Route でバックエンドプロキシを実装
  • 3タブUIで R2E API を体験できるUIを追加
  • Render のコールドスタートに対応
  • Vercel 環境変数で開発/本番を切り替え

実際に /portfolio/r2e にアクセスして、ビジネス課題から論文探索までの体験を試してみてください!

参考リンク

Top comments (0)