OpenAI API のサンプルWebアプリを Next.js から Nuxt 3 + TypeScript に1時間足らずで作り変えた話
ふと 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)
シンプルですね。
ちなみに僕は 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 install
と yarn dev
するだけで試せます。
Nuxt 3 で作り変える
Nuxt 3 で作り変えたいと思います。
主につぎの点を書き換えます。
- CSS を
app.vue
内に記述する -
useRuntimeConfig()
で.env
を扱う -
/server/api/generate.post.ts
によりサーバー側で OpenAI の API を使用する - コンポーネントから
useFetch
を使用しデータ取得を行う - 英単語の Capitalize は Computed Property で行う
app.vue
内に記述する
CSS を 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
を扱うことが可能です。
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.ts
と article.post.ts
のように使用することも可能です)
このファイルは(Server Middleware として)サーバー側で実行されます。
そのため、ユーザーに知られたくない(API キーのような)情報を使うことが可能です。
コンポーネント等、クライアント側で useFetch()
を使用すると、サーバー側にリクエストがとび実行されます。
今回のようなAPIでは コンポーネント(クライアント側)
↔ API Middleware(サーバー側)
↔ OpenAI サーバー
のようにデータを取得します。
defineEventHandler()
という便利なものが用意されていて、リクエストの情報もレスポンスの情報もとても効率的に扱えます。
レスポンスは return
するだけですし、オブジェクトを返せば自動的にJSONになります。
リクエストボディも const { animal = '' } = await readBody(event)
のように記述可能です。
実際のソースコードは次のようになります。
defineEventHandler() による Server Middleware
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>は省略)
<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