🐥

OpenAI API のサンプルWebアプリを Next.js から Nuxt 3 + TypeScript に1時間足らずで作り変えた話

2022/11/06に公開

ふと OpenAI の GPT-3 による自然言語処理を体験してみたくなり、アカウントを作成したのが今日の13:30

その2時間後には OpenAI のサンプルアプリを Next.js から Nuxt 3 + TypeScript に書き換えてました。
思いつきで OpenAI を試してみたくなり、チュートリアルを読んだら触ってみたくなり、Next.js のソースコードを見たら Nuxt 3 にしたくなりました。
それでもしばらく Open AI を試していたのですが、なぜか気づいたら Nuxt 3 化し始め、あっさりできたのでしばらく疎かにしてた Zenn の記事を書こうと思い立ったのが今(16:00)

なぜ Nuxt 3 で作りたくなったのか

Nuxt 3 になって <script setup> は使えるし useFetch() で簡潔にAPIを扱えるし、APIルーティングも使えるし、ということで「もっと把握しやすいコードになりそう」と思った次第。

ここ1ヶ月くらい Nuxt 3 のコードを触ってなかったこともあり、もうすぐ Stable リリースありそうだし。

OpenAI のサンプルコード (Next.js)

https://github.com/openai/openai-quickstart-node

シンプルですね。
ちなみに僕は Next.js でアプリケーションを作成したことはありません。
公式ドキュメント等は読んでいますが、完全にエアプです。

コード読んで気になったのは TypeScript ではない点でした。
Nuxt 3 は Vite によって 設定無しで TypeScript で書けます
(OpenAI のライブラリはもちろん TypeScript 対応していました)

OpenAI を試してみたい方

このサンプルアプリはペットの名前をつけるような用途のもの。
動物の種類を英語で入力すると、英語の名前を3つほど提案してくれます。

例: 🐎 horse → Spirit of the West, Lightning Hooves, Mighty Stardust

ちなみに裏側では次のような Prompt が作成されています。

Suggest three names for an animal that is a superhero.

Animal: Cat
Names: Captain Sharpclaw, Agent Fluffball, The Incredible Feline
Animal: Dog
Names: Ruff the Protector, Wonder Canine, Sir Barks-a-Lot
Animal: ${animal}
Names:

これを実行するだけであればリポジトリをクローンして .env に API Key を書いた上で yarn installyarn dev するだけで試せます。

https://github.com/monsat/openai-quickstart-nuxt3

Nuxt 3 で作り変える

Nuxt 3 で作り変えたいと思います。
主につぎの点を書き換えます。

  • CSS を app.vue 内に記述する
  • useRuntimeConfig().env を扱う
  • /server/api/generate.post.ts によりサーバー側で OpenAI の API を使用する
  • コンポーネントから useFetch を使用しデータ取得を行う
  • 英単語の Capitalize は Computed Property で行う

CSS を app.vue 内に記述する

app.vue は Nuxt 3 の新しいエンドポイント的なコンポーネントです。
レイアウトコンポーネントよりも上位にあり、1リクエストにつき1度しか呼ばれません。
(以降 app.vue が SPA として CSR すると考えればよいと思います)
また、今回のような1ページのサイトであれば、ページコンポーネントを用意する必要がありません。

Vue.js は SFC(Single File Component) により <style> タグを使用可能です。
元のソースコードの /pages/index.module.css 内の記述をまるっと <style scoped></style> 内に記述しました。

useRuntimeConfig().env を扱う

Nuxt 3 は nuxt.config.ts にて次のように記述することで useRuntimeConfig().env を扱うことが可能です。

nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    openaiApiKey: process.env.OPENAI_API_KEY,
  },
})

利用する際は次のように利用します。

const { openaiApiKey } = useRuntimeConfig()
// もしくは useRuntimeConfig().openaiApiKey でも可

コンポーネントや Composable Function だけでなく、今回のように Server Middleware による利用も可能です。

/server/api/generate.post.ts によりサーバー側で OpenAI の API を使用する

Nuxt 3 では /server/api/* 以下のファイルは localhost:3000/api/~~~ のようにアクセスできます。
(自動的にルーティングが設定されます)

また拡張子を .ts から .post.ts にすることで POST メソッドのリクエストのみを取り扱うことも可能です。
(たとえば article.get.tsarticle.post.ts のように使用することも可能です)

このファイルは(Server Middleware として)サーバー側で実行されます。
そのため、ユーザーに知られたくない(API キーのような)情報を使うことが可能です。

コンポーネント等、クライアント側で useFetch() を使用すると、サーバー側にリクエストがとび実行されます。
今回のようなAPIでは コンポーネント(クライアント側)API Middleware(サーバー側)OpenAI サーバー のようにデータを取得します。

defineEventHandler() という便利なものが用意されていて、リクエストの情報もレスポンスの情報もとても効率的に扱えます。
レスポンスは return するだけですし、オブジェクトを返せば自動的にJSONになります。
リクエストボディも const { animal = '' } = await readBody(event) のように記述可能です。

実際のソースコードは次のようになります。

defineEventHandler() による Server Middleware
server/api/generate.post.ts
import { Configuration, OpenAIApi } from 'openai'

const generatePrompt = (animal: string) => `
Suggest three names for an animal that is a superhero.

Animal: Cat
Names: Captain Sharpclaw, Agent Fluffball, The Incredible Feline
Animal: Dog
Names: Ruff the Protector, Wonder Canine, Sir Barks-a-Lot
Animal: ${animal}
Names:
`.trim()


export default defineEventHandler(async (event) => {
  const { openaiApiKey: apiKey } = useRuntimeConfig()
  const { animal = '' } = await readBody(event)

  if (!animal) {
    return event.res.end('No animal provided')
  }

  const configuration = new Configuration({ apiKey })
  const openai = new OpenAIApi(configuration)

  // OpenAI config
  const model = 'text-davinci-002'
  const prompt = generatePrompt(animal)
  const temperature = 0.6

  const completion = await openai.createCompletion({
    model,
    prompt,
    temperature,
  })

  const result = completion.data.choices[0].text || ''

  return {
    result,
  }
})

コンポーネントから useFetch を使用しデータ取得を行う

データ取得は useFetch() を使用します。
また、せっかくなので「英単語の Capitalize は Computed Property で行う」ようにしました。

app.vue の内容(<style>は省略)
app.vue
<script setup lang="ts">
const name = ref('')
const result = ref('')

const animal = computed(() => name.value[0].toUpperCase() + name.value.slice(1).toLowerCase())

const submit = async () => {
  result.value = ''

  const { data } = await useFetch('/api/generate', {
    method: 'POST',
    body: { animal: animal.value },
    initialCache: false,
  })

  result.value = data.value?.result || 'Sorry, Error has occurred'
}
</script>

<template>
  <div>
    <Head>
      <Title>OpenAI Quickstart</Title>
      <Link rel="icon" href="/dog.png" />
    </Head>

    <main class="main">
      <img src="/dog.png" class="icon" />
      <h3>Name my pet</h3>
      <form @submit.prevent="submit">
        <input v-model="name" placeholder="Enter an animal" />
        <input type="submit" value="Generate names" />
      </form>
      <div class="result">{{ result }}</div>
    </main>
  </div>
</template>

ロジックをhtmlから追い出して、すっきりしましたね。
今回は使用しませんでしたが、適宜 Composable Function を使用することにより、ロジックをコンポーネントの外に出すとさらに見通しが良くなります。

まとめ

というわけで OpenAI API のサンプルWebアプリを Next.js から Nuxt 3 + TypeScript に1時間足らずで作り変えた話でした。
後半は Nuxt 3 を学習したいという方のために、なるべく基礎的なところも書きました。
分かりづらいところがあればご指摘ください。追加したいと思います。

今回は TypeScript 固有の記述はほとんどしていませんが、ゼロコンフィグで TypeScript による開発ができています。
機能向上と同様、開発体験の向上も Nuxt 3 の優位性だと思います。
ぜひ活用していきたいですね。

Discussion