【AI駆動開発】ChatGPT、v0、Cursorを組み合わせて爆速でアプリ開発してみよう!(バックエンド実装も含む) #ai-driven-dev #アプリ開発
バックエンドの実装にもAIを活用したい
前回の記事(【AI駆動開発】V0ならこんなに簡単!UIデザインからフロント実装までが爆速に!)では、v0でAIを用いてUIデザインを作成し、その画面をベースにフロントの実装までを行ってみました。今回は、その続きでバックエンドの実装を進めてみたいと思います。ChatGPTとCursorを使ってみます。
前回の振り返り
前回は、自然言語で指示するとUIを生成してくれるv0を使って、下記のような画面を作成しました。
v0で生成した結果をReactのコンポーネントとして取り込み、実際にフロントだけで機能するタイマーを実装しました。
やること
前回作ったアプリをベースにして、以下のような機能を実装していきます。
- ブラウザを再起動してもカウントダウン状態が維持されるようにする。
- 認証は、ログインなしでCookieのみ。
Next.jsのアプリなので、その中にバックエンドの機能も入れていきます。
インターフェイスはRESTで、OpenAPIの定義はChatGPTに生成させます。それをベースにしてopenapi-generator-cliでTypeScriptの型定義を生成し、Cursorを使って機能を実装していきます。
バックエンドの実装
openapi.yamlの生成
ChatGPTに、v0の画面をキャプチャーと一緒に下記のプロンプトを与えました。
この画面に対して、以下のような機能を追加したいので、OpenAPIのスキーマ定義を作成してください。
- ブラウザを再起動してもカウントダウン状態が維持されるようにする。
- 認証はログインなしでCookieのみ。
生成されたコードは下記の通りです。
openapi: 3.0.0
info:
title: カウントダウンタイマーAPI
version: "1.0.0"
paths:
/timer:
get:
summary: タイマーの現在の状態を取得する
description: ブラウザからタイマーの現在のカウントダウン状態を取得する。
responses:
'200':
description: タイマー状態の取得に成功。
content:
application/json:
schema:
$ref: '#/components/schemas/TimerState'
security:
- cookieAuth: []
post:
summary: タイマーを開始または再開する
description: 指定された時間でタイマーを開始または再開する。
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TimerRequest'
responses:
'200':
description: タイマーが正常に開始された。
content:
application/json:
schema:
$ref: '#/components/schemas/TimerState'
security:
- cookieAuth: []
components:
schemas:
TimerState:
type: object
properties:
hours:
type: integer
format: int32
example: 1
minutes:
type: integer
format: int32
example: 30
seconds:
type: integer
format: int32
example: 45
running:
type: boolean
example: true
TimerRequest:
type: object
properties:
hours:
type: integer
format: int32
minutes:
type: integer
format: int32
seconds:
type: integer
format: int32
securitySchemes:
cookieAuth:
type: apiKey
in: cookie
name: timerAuth
時分秒を保存する機能、それらを読み込む機能、Cookieでの認証が含まれており、問題なさそうです。特に変更することなく、このまま採用することにしましょう。なお、認証周りは当初理解できませんでしたが、ChatGPTに質問して理解し、問題がなさそうなことを確認しました。
TypeScriptの型定義の生成
次は下記のコマンドで、openapi.yamlからTypeScriptの型定義を生成します。
npm install @openapitools/openapi-generator-cli -D
node_modules/.bin/openapi-generator-cli generate -i openapi.yaml -g typescript-fetch -o openapi
ここでは、生成させるものとして何を指定すればよいか分からなかったため、ChatGPTに質問し、typescript-fetch
を採用しました。与えたプロンプトは「openapi-generator-cliでNext.js用の型定義を生成したい。-gには何を指定すればよいか。」です。
認証の実装
データの保存・読み込みはユーザーに紐付けて行う必要があるため、まずは認証機能から実装していきます。このあたりはNext.jsの知識を元に「公式通りにミドルウェアを作ればよいはず」というのが分かったので、Next.jsの公式ドキュメントを参照しながら、middleware.tsを実装しました。Cookie名とAPIエンドポイントはopenapi.yamlの内容に合わせてあります。
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const auth = request.cookies.get('timerAuth');
if (auth === undefined) {
return new Response('Unauthorized', { status: 401 })
}
}
export const config = {
matcher: '/timer/:path*',
}
最初、matcherの書き方が分からずに /timer/*
としていたのですが、正しく機能しませんでした。Cursorに質問し、上記の正しい書き方に修正することができました。また、Cookie情報の取得方法もCursorに助けてもらいました。
データ読み込み機能・保存機能の実装(API側)
以下のように実装し、Postmanを使って動作確認を行いました。ここは普通のロジック実装のみのため、AIに頼らず自力でサクッと実装しました。
import { TimerRequest, TimerState } from '@/openapi';
import type { NextRequest } from 'next/server'
const timers = new Map<string, TimerState>();
export async function GET(req: NextRequest) {
const auth = getAuth(req);
// auth.valueに対応するデータがあればそれを返す。なければデフォルト値を返す。
const timer = timers.get(auth.value);
if (timer !== undefined) {
return new Response(JSON.stringify(timer));
}
const defaultTimer: TimerState = {
hours: 0,
minutes: 0,
seconds: 0,
running: true,
}
return new Response(JSON.stringify(defaultTimer));
}
export async function POST(req: NextRequest) {
const auth = getAuth(req);
// auth.valueに対応するデータとしてbodyを保存する。
const body = await req.json() as TimerRequest;
const timer = {
...body,
running: true,
};
timers.set(
auth.value,
timer,
);
return new Response(JSON.stringify(timer));
}
const getAuth = (req: NextRequest) => {
const auth = req.cookies.get('timerAuth');
if (auth === undefined) {
throw new Error();
}
return auth;
}
データ読み込み機能・保存機能の実装(フロント側)
フロント側の実装に移りましょう。まずは認証情報の設定です。下記のカスタムフックを実装し、コンポーネントの先頭に呼び出しを追加しました。
function useAuth(): void {
const [cookies, setCookie] = useCookies(['timerAuth']);
if (cookies.timerAuth === undefined) {
const uuid = uuidv4();
setCookie('timerAuth', uuid);
}
}
次に、APIクライアントを作成しておきます。
const config = new Configuration({
basePath: 'http://localhost:3000',
});
const api = new DefaultApi(config);
今度はデータ読み込み機能の実装です。
useEffect(() => {
(async function() {
const timerState = await api.timerGet();
setGoalTime(new Date(Date.now() + (timerState.hours ?? 0) * 60 * 60 * 1000 + (timerState.minutes ?? 0) * 60 * 1000 + (timerState.seconds ?? 0) * 1000));
setPausing(false);
})();
}, [])
Postmanでデータを保存しておくと、ブラウザを開いた際にその残り時間からスタートできることを確認しました。順調です。
続いてカウントダウン開始時のデータ保存です。
<Button className="bg-blue-700 text-white w-24"
onClick={() => {
setGoalTime(new Date(Date.now() + hour * 60 * 60 * 1000 + minute * 60 * 1000 + second * 1000));
setPausing(false);
const timer: TimerRequest = {
hours: hour,
minutes: minute,
seconds: second,
}
api.timerPost({timerRequest: timer});
}}
>スタート</Button>
実装できたと思うので動作確認をすると、以下のような挙動になってしまいました。
- 残り3分から開始。
- (ここで、残り3分というデータがサーバーに保存される。)
- 残り2分の時点で画面をリロード。
- (ここで、サーバーに保存されている「残り3分」というデータが読み込まれる。)
- 残り3分から再開。本来は、残り2分から再開して欲しいところでした。
不具合の原因は、上記の通り「開始時点の残り時間(だけ)」をサーバーに保存し、画面読み込み時にその状態から再開していたことです。そうではなく「終了時刻」を保存するようにしておく必要がありました。それを実現するためには下記の2通りが考えられます。
- フロントからは残り時間をポストし、サーバー上で終了時刻を計算して保存する。
- データ読み込み時はサーバー上で残り時間を計算して返す。
- フロントから終了時刻をポストし、サーバー上ではそのまま保存する。
- データ読み込み時は、保存してある終了時刻をそのまま返し、フロント上で状態を復元する。
RESTのI/Fを変更しないのであれば前者の実装になりますが、機能面で考えると後者の方がシンプルでよいでしょう。通常であれば後者の実装にするところでしたが、ChatGPTが生成したopenapi.yamlの内容が一見よさそうだったので、精査・検討せずに採用してしまったのが間違いの元でした。
一時停止・再開の実装(フロント側)
openapi.yamlに一時停止・再開のAPIがないことに気づきました。そもそも終了時刻を保存するようにすれば、一時停止・再開はAPIの実装も不要なのですが、今は残り時間を保存するように実装しています。そう言えばopenapi.yamlでは一時停止状態もポストされるようになっています。これはどのように扱えばよいのでしょうか?サーバー上で終了時刻を計算して保存するように変更するという手もありますが、既に設計が想定外の状態になっているため、今回のチャレンジはここまでとしましょう。これも、openapi.yamlの内容を精査・検討していれば防げたはずでした。
うまくいったところ・うまくいかなかったところ
うまくいったところ・うまくいかなかったところを纏めると以下のようになります。
- UI設計~フロント実装~API設計~API実装までの一連の流れについてAIの補助を受けながら爆速で開発することができました。
- Cursorを使うと、ChatGPTとVSCodeを行き来しなくてすむので、とても楽です。
- エラー修正などで既存コードを参照させる際は、特に楽です。
- 先述の通り、APIのI/F設計とサーバー上で何を保存しておくのかという設計が不足していました。
- CursorやChatGPTは「とりあえず動くもの」を作るには早いのですが、それが適切な作りかどうかはかなり不安になります。「こう書けば動く」を教えてくれるので理由や意味を調べることを疎かにしがちなのです。その結果、今回のように不適切な設計・実装になってしまうこともあります。
- react-cookieパッケージの型定義が認識されず
any
扱いになってしまいました。Cursorで解決を試みましたが、解決できませんでした。 - Next.jsでフロントもバックエンドも実装している都合上、整理のためにAPIエンドポイントは
/api/timer
としたかったのですが、openapi.yamlでは/timer
になっていました。/api/timer
にしたいというのは実装時に気づいたのですが、openapi.yamlを変更するのもopenapi.yamlに合わせないのもイマイチだと思ったので、今回は/timer
のままにしました。 - コードのフォーマットがグチャグチャになりました。また、何度も不要な
import
などが残っていてコミットし直しました。最初にPrettierとESLintを入れておくべきでした。- ChatGPTやCursorは、何をしたいかを説明すれば助けてくれますが、何をすべきかは教えてくれません。何をすべきかも聞けば教えてくれるのですが、聞くという発想が漏れてしまった時に指摘してくれないのです。このあたりは「常にレビューを行う」などと同じように「常に漏れがないかを質問する」などという文化に変わっていく必要があるかもしれません。
- Cursorは、自動で編集してくれるのは楽なのですが、既存の実装が壊されることも多くあります。うまくいかない場合は、
Ctrl
+K
で直接編集させるのではなくCtrl
+L
で質問して、必要な部分だけ採用する方がよいでしょう。
まとめ
UI設計(Reactコードの生成)・APIのI/F定義生成・コードの実装といった、あらゆる局面でAI・LLMが力を発揮しています。高速に・楽にアプリ開発を進めることができます。今後もさらなるAI・LLMの進化により更に便利になると思われます。
しかしながら、エンジニアが不要になるわけではありません。今回のチャレンジでも、設計が不十分だったことが原因で実装時に困ったことになってしまいました。これからの時代は、AIに適切に生成させる力とともに、AIの生成結果を吟味し、必要に応じて修正を行い、アプリ開発全体でAIを最大限に活用する力がエンジニアに求めらることになるのではないでしょうか。
勉強会のお知らせ
「AI駆動開発(AI-Driven Development) 勉強会(第1回)」を2024年2月2日(金)に開催予定です。今後の生成AI・LLMを利用した開発スタイルについて共有・議論したいと思いますので、本記事の内容に興味を持っていただける方はぜひご参加いただければと思います!!