Cursor-3.7-sonnetとは
Anthropic社が2025年2月24日に発表した最新モデルです。
公式のNewsには
コーディングとフロントエンドWeb開発において特に大きな改善を示しています。
とはっきり書いています。これは興味をそそられますね!
CursorのYoloモードとは
みなさんご存知のAIコードエディタのCursor、normalモードとagentモードがあり、agentモードでは以下のことを連続的に実行していきます。
- コードの読み込みと書き込み
- コードベースの検索
- MCPサーバーの呼び出し
- ターミナルコマンドの実行
- 最新情報の自動ウェブ検索
ただ、コマンド実行時には確認を求める画面が出てくるので一応人間がチェックして実行する、という形になるのですが、Yoloモードはこのagentモードにあるターミナルコマンドを自動で実行するので、とにかくコマンド打ってコード書いてガンガン実装していきます。公式(文末参照)にはテスト実行などに向いている、と書いてありますが今回は1からアプリを作らせてみたいと思います。
※コードもほぼ全部記載していますが、かなり長いのでコードの部分はある程度読み飛ばしていただくと良いと思います。
Cursorの設定
右上の歯車から設定画面を開き、FeaturesでEnable yolo modeのチェックを入れるだけです。
Yoloモードは、あなたが設定したallowlistとdenylistに基づいてコマンドを実行します。外部ソースからの潜在的なプロンプト・インジェクションのリスクに注意し、自己責任で使用してください。
という警告のポップアップが出てきますが、continueで閉じておきます。
例の如くToDoアプリ作らせてみた
NextJS環境構築
空のディレクトリを用意して、Cursorで開きます。
そしてCOMPOSERで以下のプロンプトを入力します。左下でモデルをclaude-3.7-sonnetを指定し、右下でagentモードを選んでsubmitします。
フロントエンド環境構築
まず、NextJS/TypeScriptのフロントエンドとAWS CDKを使ったサーバレスバックエンドのアプリケーションを作成していきます。最初にプロジェクトの構造を確認しましょう。
ワークスペースが空のようですので、新しいプロジェクトを作成していきます。まずはNext.jsのフロントエンドプロジェクトを作成しましょう。
ディレクトリの中身を見てプロジェクト作成から行なってくれるみたいです。
1 2 3 4 5 6 7 | $ npx create-next-app@latest frontend --typescript --eslint --tailwind --app --src-dir --import-alias "@/*" Need to install the following packages: create-next-app@15.2.1 Ok to proceed? (y) |
止まってしまったようなのでキャンセルして再実行すると指示通りyesで進めてくれました。
コマンドが中断されたようです。指示通りに「yes」で進めるために、再度コマンドを実行します。
1 | $ echo "y" | npx create-next-app@latest frontend --typescript --eslint --tailwind --app --src-dir --import-alias "@/*" |
CDK
次に、AWS CDKを使用してバックエンドのインフラストラクチャを作成します。まずはCDKプロジェクトを初期化しましょう。
1 | $ dir backend && cd backend && npx aws-cdk init app --language typescript |
プロジェクトの基本構造ができました。次に、バックエンドのCDKスタックを実装して、API Gateway、Lambda、DynamoDBのリソースを定義します。まず、必要なパッケージをインストールしましょう。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 | $ npm install aws-cdk-lib @aws-cdk/aws-apigateway @aws-cdk/aws-lambda @aws-cdk/aws-dynamodb @types/aws-lambda npm WARN deprecated npm WARN deprecated For more information on how to migrate, see https://docs.aws.amazon.com/cdk/v2/guide/mig rating-v2.html added 78 packages, and audited 461 packages in 22s 35 packages are looking for funding run `npm fund` for details 36 high severity vulnerabilities To address all issues, run: npm audit fix Run `npm audit` for details. |
CDK v1のパッケージが非推奨になっているという警告が表示されています。CDK v2を使用するように修正しましょう。まず、CDKスタックを実装します。
勝手にv2を使用するように修正してくれました。
CDKスタックを実装して、API Gateway、Lambda、DynamoDBのリソースを定義します。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as apigateway from 'aws-cdk-lib/aws-apigateway'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import * as path from 'path'; export class BackendStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // DynamoDB テーブルの作成 const todoTable = new dynamodb.Table(this, 'TodoTable', { partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, removalPolicy: cdk.RemovalPolicy.DESTROY, // 開発環境用の設定(本番環境では注意) }); // Lambda 関数のディレクトリ const lambdaDir = path.join(__dirname, '../lambda'); // Todo API用のLambda関数 const todoApiHandler = new lambda.Function(this, 'TodoApiHandler', { runtime: lambda.Runtime.NODEJS_18_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(lambdaDir, 'todo-api')), environment: { TODO_TABLE_NAME: todoTable.tableName, }, }); // Lambda関数にDynamoDBへのアクセス権限を付与 todoTable.grantReadWriteData(todoApiHandler); // API Gatewayの作成 const api = new apigateway.RestApi(this, 'TodoApi', { restApiName: 'Todo API', description: 'API for Todo application', defaultCorsPreflightOptions: { allowOrigins: apigateway.Cors.ALL_ORIGINS, allowMethods: apigateway.Cors.ALL_METHODS, }, }); // API Gatewayのリソースとメソッドの設定 const todos = api.root.addResource('todos'); // GET /todos - すべてのTodoを取得 todos.addMethod('GET', new apigateway.LambdaIntegration(todoApiHandler)); // POST /todos - 新しいTodoを作成 todos.addMethod('POST', new apigateway.LambdaIntegration(todoApiHandler)); // 個別のTodoリソース const todo = todos.addResource('{id}'); // GET /todos/{id} - 特定のTodoを取得 todo.addMethod('GET', new apigateway.LambdaIntegration(todoApiHandler)); // PUT /todos/{id} - 特定のTodoを更新 todo.addMethod('PUT', new apigateway.LambdaIntegration(todoApiHandler)); // DELETE /todos/{id} - 特定のTodoを削除 todo.addMethod('DELETE', new apigateway.LambdaIntegration(todoApiHandler)); // API GatewayのURLをエクスポート new cdk.CfnOutput(this, 'ApiUrl', { value: api.url, description: 'The URL of the API Gateway', }); } } |
バックエンド(Lambda)
次に、Lambda関数を実装します。まず、Lambda関数用のディレクトリを作成し、必要なファイルを作成します。
(中略)
次に、Lambda関数のメインコードを実装します。
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; import * as AWS from 'aws-sdk'; import { v4 as uuidv4 } from 'uuid'; // DynamoDBクライアントの初期化 const dynamoDB = new AWS.DynamoDB.DocumentClient(); const TABLE_NAME = process.env.TODO_TABLE_NAME || ''; // Todoアイテムの型定義 interface Todo { id: string; title: string; description?: string; completed: boolean; createdAt: string; updatedAt: string; } // レスポンスを生成するヘルパー関数 const createResponse = (statusCode: number, body: any): APIGatewayProxyResult => { return { statusCode, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': true, }, body: JSON.stringify(body), }; }; // すべてのTodoを取得 const getAllTodos = async (): Promise<APIGatewayProxyResult> => { try { const result = await dynamoDB.scan({ TableName: TABLE_NAME, }).promise(); return createResponse(200, result.Items); } catch (error) { console.error('Error getting all todos:', error); return createResponse(500, { message: 'Error getting todos' }); } }; // 特定のTodoを取得 const getTodoById = async (id: string): Promise<APIGatewayProxyResult> => { try { const result = await dynamoDB.get({ TableName: TABLE_NAME, Key: { id }, }).promise(); if (!result.Item) { return createResponse(404, { message: 'Todo not found' }); } return createResponse(200, result.Item); } catch (error) { console.error(`Error getting todo ${id}:`, error); return createResponse(500, { message: 'Error getting todo' }); } }; // 新しいTodoを作成 const createTodo = async (data: any): Promise<APIGatewayProxyResult> => { try { const timestamp = new Date().toISOString(); const todo: Todo = { id: uuidv4(), title: data.title, description: data.description || '', completed: false, createdAt: timestamp, updatedAt: timestamp, }; await dynamoDB.put({ TableName: TABLE_NAME, Item: todo, }).promise(); return createResponse(201, todo); } catch (error) { console.error('Error creating todo:', error); return createResponse(500, { message: 'Error creating todo' }); } }; // Todoを更新 const updateTodo = async (id: string, data: any): Promise<APIGatewayProxyResult> => { try { // まず、Todoが存在するか確認 const existingTodo = await dynamoDB.get({ TableName: TABLE_NAME, Key: { id }, }).promise(); if (!existingTodo.Item) { return createResponse(404, { message: 'Todo not found' }); } const timestamp = new Date().toISOString(); const updatedTodo = { ...existingTodo.Item, title: data.title || existingTodo.Item.title, description: data.description !== undefined ? data.description : existingTodo.Item.description, completed: data.completed !== undefined ? data.completed : existingTodo.Item.completed, updatedAt: timestamp, }; await dynamoDB.put({ TableName: TABLE_NAME, Item: updatedTodo, }).promise(); return createResponse(200, updatedTodo); } catch (error) { console.error(`Error updating todo ${id}:`, error); return createResponse(500, { message: 'Error updating todo' }); } }; // Todoを削除 const deleteTodo = async (id: string): Promise<APIGatewayProxyResult> => { try { // まず、Todoが存在するか確認 const existingTodo = await dynamoDB.get({ TableName: TABLE_NAME, Key: { id }, }).promise(); if (!existingTodo.Item) { return createResponse(404, { message: 'Todo not found' }); } await dynamoDB.delete({ TableName: TABLE_NAME, Key: { id }, }).promise(); return createResponse(200, { message: 'Todo deleted successfully' }); } catch (error) { console.error(`Error deleting todo ${id}:`, error); return createResponse(500, { message: 'Error deleting todo' }); } }; // Lambda関数のハンドラー export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => { console.log('Event:', JSON.stringify(event, null, 2)); try { const { httpMethod, resource, pathParameters, body } = event; const data = body ? JSON.parse(body) : {}; // リクエストのパスとメソッドに基づいて適切な処理を実行 if (resource === '/todos' && httpMethod === 'GET') { return await getAllTodos(); } if (resource === '/todos' && httpMethod === 'POST') { return await createTodo(data); } if (resource === '/todos/{id}' && httpMethod === 'GET' && pathParameters?.id) { return await getTodoById(pathParameters.id); } if (resource === '/todos/{id}' && httpMethod === 'PUT' && pathParameters?.id) { return await updateTodo(pathParameters.id, data); } if (resource === '/todos/{id}' && httpMethod === 'DELETE' && pathParameters?.id) { return await deleteTodo(pathParameters.id); } // サポートされていないエンドポイント return createResponse(404, { message: 'Not found' }); } catch (error) { console.error('Error processing request:', error); return createResponse(500, { message: 'Internal server error' }); } }; |
tsconfig.jsonを修正して、linterエラーを解決します。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | { "compilerOptions": { "target": "ES2018", "module": "commonjs", "lib": ["es2018", "dom"], "declaration": true, "strict": true, "noImplicitAny": true, "strictNullChecks": true, "noImplicitThis": true, "alwaysStrict": true, "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": false, "inlineSourceMap": true, "inlineSources": true, "experimentalDecorators": true, "strictPropertyInitialization": false, "typeRoots": ["./node_modules/@types"], "outDir": "dist", "esModuleInterop": true }, "exclude": ["node_modules", "dist"] } |
勝手にlinterエラーも修正してくれました。
次に、Lambda関数の依存関係をインストールします。
フロントエンド実装
次に、フロントエンドのTodoアプリケーションを実装します。まず、フロントエンドのディレクトリに移動します。
フロントエンドのTodoアプリケーションを実装します。まず、APIクライアントを作成します。
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 | // APIのベースURL(デプロイ後に実際のAPIエンドポイントに置き換える) const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api'; // Todoの型定義 export interface Todo { id: string; title: string; description?: string; completed: boolean; createdAt: string; updatedAt: string; } // 新しいTodoの作成用の型 export interface CreateTodoInput { title: string; description?: string; } // Todoの更新用の型 export interface UpdateTodoInput { title?: string; description?: string; completed?: boolean; } // すべてのTodoを取得 export const getAllTodos = async (): Promise<Todo[]> => { try { const response = await fetch(`${API_BASE_URL}/todos`); if (!response.ok) { throw new Error('Failed to fetch todos'); } return await response.json(); } catch (error) { console.error('Error fetching todos:', error); return []; } }; // 特定のTodoを取得 export const getTodoById = async (id: string): Promise<Todo | null> => { try { const response = await fetch(`${API_BASE_URL}/todos/${id}`); if (!response.ok) { throw new Error('Failed to fetch todo'); } return await response.json(); } catch (error) { console.error(`Error fetching todo ${id}:`, error); return null; } }; // 新しいTodoを作成 export const createTodo = async (todo: CreateTodoInput): Promise<Todo | null> => { try { const response = await fetch(`${API_BASE_URL}/todos`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(todo), }); if (!response.ok) { throw new Error('Failed to create todo'); } return await response.json(); } catch (error) { console.error('Error creating todo:', error); return null; } }; // Todoを更新 export const updateTodo = async (id: string, todo: UpdateTodoInput): Promise<Todo | null> => { try { const response = await fetch(`${API_BASE_URL}/todos/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(todo), }); if (!response.ok) { throw new Error('Failed to update todo'); } return await response.json(); } catch (error) { console.error(`Error updating todo ${id}:`, error); return null; } }; // Todoを削除 export const deleteTodo = async (id: string): Promise<boolean> => { try { const response = await fetch(`${API_BASE_URL}/todos/${id}`, { method: 'DELETE', }); if (!response.ok) { throw new Error('Failed to delete todo'); } return true; } catch (error) { console.error(`Error deleting todo ${id}:`, error); return false; } }; |
次に、Todoフォームコンポーネントを作成します。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | import React, { useState, useEffect } from 'react'; import { Todo, CreateTodoInput, UpdateTodoInput } from '../api/todoApi'; interface TodoFormProps { onSubmit: (todo: CreateTodoInput | UpdateTodoInput) => void; initialData?: Todo; isEditing: boolean; onCancel?: () => void; } const TodoForm: React.FC<TodoFormProps> = ({ onSubmit, initialData, isEditing, onCancel }) => { const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); useEffect(() => { if (initialData) { setTitle(initialData.title); setDescription(initialData.description || ''); } else { setTitle(''); setDescription(''); } }, [initialData]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!title.trim()) { alert('タイトルを入力してください'); return; } onSubmit({ title: title.trim(), description: description.trim() || undefined, }); if (!isEditing) { setTitle(''); setDescription(''); } }; return ( <form onSubmit={handleSubmit} className="p-4 mb-6 bg-white rounded-lg shadow"> <h2 className="mb-4 text-xl font-bold text-gray-800"> {isEditing ? 'Todoを編集' : '新しいTodoを追加'} </h2> <div className="mb-4"> <label htmlFor="title" className="block mb-2 text-sm font-medium text-gray-700"> タイトル* </label> <input type="text" id="title" value={title} onChange={(e) => setTitle(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Todoのタイトルを入力" required /> </div> <div className="mb-4"> <label htmlFor="description" className="block mb-2 text-sm font-medium text-gray-700"> 説明 </label> <textarea id="description" value={description} onChange={(e) => setDescription(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Todoの説明を入力(任意)" rows={3} /> </div> <div className="flex justify-end space-x-2"> {isEditing && onCancel && ( <button type="button" onClick={onCancel} className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2" > キャンセル </button> )} <button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" > {isEditing ? '更新' : '追加'} </button> </div> </form> ); }; export default TodoForm; |
次に、メインのTodoリストコンポーネントを作成します。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import React, { useState, useEffect } from 'react'; import { Todo, getAllTodos, createTodo, updateTodo, deleteTodo } from '../api/todoApi'; import TodoItem from './TodoItem'; import TodoForm from './TodoForm'; const TodoList: React.FC = () => { const [todos, setTodos] = useState<Todo[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); const [editingTodo, setEditingTodo] = useState<Todo | null>(null); // Todoリストを取得 const fetchTodos = async () => { setLoading(true); try { const data = await getAllTodos(); setTodos(data); setError(null); } catch (err) { setError('Todoの取得に失敗しました。'); console.error(err); } finally { setLoading(false); } }; useEffect(() => { fetchTodos(); }, []); // 新しいTodoを作成 const handleCreateTodo = async (todoData: { title: string; description?: string }) => { try { const newTodo = await createTodo(todoData); if (newTodo) { setTodos([...todos, newTodo]); } } catch (err) { setError('Todoの作成に失敗しました。'); console.error(err); } }; ... (以下略) |
型エラーが出ました。
TodoFormコンポーネントを修正して、型の互換性の問題を解決します。
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 | import React, { useState, useEffect } from 'react'; import { Todo, CreateTodoInput, UpdateTodoInput } from '../api/todoApi'; interface TodoFormProps { onSubmit: (todo: CreateTodoInput | UpdateTodoInput) => Promise<void>; initialData?: Todo; isEditing: boolean; onCancel?: () => void; } const TodoForm: React.FC<TodoFormProps> = ({ onSubmit, initialData, isEditing, onCancel }) => { const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); useEffect(() => { if (initialData) { setTitle(initialData.title); setDescription(initialData.description || ''); } else { setTitle(''); setDescription(''); } }, [initialData]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!title.trim()) { alert('タイトルを入力してください'); return; } const todoData: CreateTodoInput | UpdateTodoInput = isEditing ? { title: title.trim(), description: description.trim() || undefined, } : { title: title.trim(), description: description.trim() || undefined, }; await onSubmit(todoData); if (!isEditing) { setTitle(''); setDescription(''); } }; return ( <form onSubmit={handleSubmit} className="p-4 mb-6 bg-white rounded-lg shadow"> <h2 className="mb-4 text-xl font-bold text-gray-800"> {isEditing ? 'Todoを編集' : '新しいTodoを追加'} </h2> <div className="mb-4"> <label htmlFor="title" className="block mb-2 text-sm font-medium text-gray-700"> タイトル* </label> <input type="text" id="title" value={title} onChange={(e) => setTitle(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Todoのタイトルを入力" required /> </div> <div className="mb-4"> <label htmlFor="description" className="block mb-2 text-sm font-medium text-gray-700"> 説明 </label> <textarea id="description" value={description} onChange={(e) => setDescription(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Todoの説明を入力(任意)" rows={3} /> </div> <div className="flex justify-end space-x-2"> {isEditing && onCancel && ( <button type="button" onClick={onCancel} className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2" > キャンセル </button> )} <button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" > {isEditing ? '更新' : '追加'} </button> </div> </form> ); }; export default TodoForm; |
最後に、メインページを更新して、Todoリストコンポーネントを表示します。
01 02 03 04 05 06 07 08 09 10 11 | import TodoList from './components/TodoList'; export default function Home() { return ( <main className="min-h-screen p-8 bg-gray-100"> <div className="container mx-auto"> <TodoList /> </div> </main> ); } |
最後に、APIのモックサーバーを作成して、フロントエンドのテストができるようにします。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | import { NextRequest, NextResponse } from 'next/server'; import { v4 as uuidv4 } from 'uuid'; // インメモリデータストア let todos = [ { id: '1', title: 'Next.jsを学ぶ', description: 'Next.jsの基本的な使い方を学習する', completed: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, { id: '2', title: 'AWS CDKを学ぶ', description: 'AWS CDKを使ってサーバレスアプリケーションをデプロイする方法を学ぶ', completed: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, ]; // すべてのTodoを取得 export async function GET() { return NextResponse.json(todos); } // 新しいTodoを作成 export async function POST(request: NextRequest) { const data = await request.json(); if (!data.title) { return NextResponse.json( { message: 'Title is required' }, { status: 400 } ); } const timestamp = new Date().toISOString(); const newTodo = { id: uuidv4(), title: data.title, description: data.description || '', completed: false, createdAt: timestamp, updatedAt: timestamp, }; todos.push(newTodo); return NextResponse.json(newTodo, { status: 201 }); } |
uuidパッケージをインストールします。
1 | $ npm install uuid @types/uuid |
個別のTodoを操作するためのAPIエンドポイントも追加します。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | import { NextRequest, NextResponse } from 'next/server'; // インメモリデータストアへの参照(実際のアプリケーションではデータベースを使用) let todos = [ { id: '1', title: 'Next.jsを学ぶ', description: 'Next.jsの基本的な使い方を学習する', completed: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, { id: '2', title: 'AWS CDKを学ぶ', description: 'AWS CDKを使ってサーバレスアプリケーションをデプロイする方法を学ぶ', completed: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, ]; // 特定のTodoを取得 export async function GET( request: NextRequest, { params }: { params: { id: string } } ) { const id = params.id; const todo = todos.find(todo => todo.id === id); if (!todo) { return NextResponse.json( { message: 'Todo not found' }, { status: 404 } ); } return NextResponse.json(todo); } // Todoを更新 export async function PUT( request: NextRequest, { params }: { params: { id: string } } ) { const id = params.id; const todoIndex = todos.findIndex(todo => todo.id === id); if (todoIndex === -1) { return NextResponse.json( { message: 'Todo not found' }, { status: 404 } ); } const data = await request.json(); const updatedTodo = { ...todos[todoIndex], title: data.title !== undefined ? data.title : todos[todoIndex].title, description: data.description !== undefined ? data.description : todos[todoIndex].description, completed: data.completed !== undefined ? data.completed : todos[todoIndex].completed, updatedAt: new Date().toISOString(), }; todos[todoIndex] = updatedTodo; return NextResponse.json(updatedTodo); } // Todoを削除 export async function DELETE( request: NextRequest, { params }: { params: { id: string } } ) { const id = params.id; const todoIndex = todos.findIndex(todo => todo.id === id); if (todoIndex === -1) { return NextResponse.json( { message: 'Todo not found' }, { status: 404 } ); } todos = todos.filter(todo => todo.id !== id); return NextResponse.json({ message: 'Todo deleted successfully' }); } |
Note: we default stop the agent after 25 tool calls. Please ask the agent to continue manually.
おっと、止まってしまったようなので続きをお願いしましょう。
続きをお願いします
フロントエンドとバックエンドの実装が完了しました。次に、フロントエンドのAPIクライアントを修正して、モックAPIを使用するように設定します。
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 | // APIのベースURL(デプロイ後に実際のAPIエンドポイントに置き換える) const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || '/api'; // Todoの型定義 export interface Todo { id: string; title: string; description?: string; completed: boolean; createdAt: string; updatedAt: string; } // 新しいTodoの作成用の型 export interface CreateTodoInput { title: string; description?: string; } // Todoの更新用の型 export interface UpdateTodoInput { title?: string; description?: string; completed?: boolean; } // すべてのTodoを取得 export const getAllTodos = async (): Promise<Todo[]> => { try { const response = await fetch(`${API_BASE_URL}/todos`); if (!response.ok) { throw new Error('Failed to fetch todos'); } return await response.json(); } catch (error) { console.error('Error fetching todos:', error); return []; } }; // 特定のTodoを取得 export const getTodoById = async (id: string): Promise<Todo | null> => { try { const response = await fetch(`${API_BASE_URL}/todos/${id}`); if (!response.ok) { throw new Error('Failed to fetch todo'); } return await response.json(); } catch (error) { console.error(`Error fetching todo ${id}:`, error); return null; } }; // 新しいTodoを作成 export const createTodo = async (todo: CreateTodoInput): Promise<Todo | null> => { try { const response = await fetch(`${API_BASE_URL}/todos`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(todo), }); if (!response.ok) { throw new Error('Failed to create todo'); } return await response.json(); } catch (error) { console.error('Error creating todo:', error); return null; } }; // Todoを更新 export const updateTodo = async (id: string, todo: UpdateTodoInput): Promise<Todo | null> => { try { const response = await fetch(`${API_BASE_URL}/todos/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(todo), }); if (!response.ok) { throw new Error('Failed to update todo'); } return await response.json(); } catch (error) { console.error(`Error updating todo ${id}:`, error); return null; } }; // Todoを削除 export const deleteTodo = async (id: string): Promise<boolean> => { try { const response = await fetch(`${API_BASE_URL}/todos/${id}`, { method: 'DELETE', }); if (!response.ok) { throw new Error('Failed to delete todo'); } return true; } catch (error) { console.error(`Error deleting todo ${id}:`, error); return false; } }; |
次に、フロントエンドアプリケーションを起動して動作確認をしましょう。
1 | $ npm run dev |
実はエラーでうまく立ち上がりませんでしたが、Cursorはまだ走っているので後で直してもらいましょう。
READMEまで?
最後に、プロジェクトのREADMEファイルを作成して、アプリケーションの概要と使い方を説明します。
これで、フロントエンドとバックエンドの両方の実装が完了しました。以下に、作成したアプリケーションの概要をまとめます:
フロントエンド(Next.js/TypeScript)
モダンなUIを持つTodoアプリケーション
Todoの作成、読み取り、更新、削除機能
Todoの完了状態の切り替え機能
レスポンシブデザイン(Tailwind CSS)
開発用のモックAPIを実装
バックエンド(AWS CDK/サーバレス)
AWS CDKを使用したインフラストラクチャのコード化
API Gateway:RESTful APIエンドポイントの提供
Lambda(TypeScript):ビジネスロジックの実装
DynamoDB:Todoデータの永続化
開発・デプロイ手順
フロントエンドの開発:cd frontend && npm run dev
バックエンドのデプロイ:cd backend && npx cdk deploy
本番環境へのデプロイ:CDKでバックエンドをデプロイし、フロントエンドをホスティングサービスにデプロイ
このアプリケーションは、開発環境ではモックAPIを使用してフロントエンドの開発を行い、本番環境ではAWSのサーバレスインフラストラクチャを使用して実行されます。
フロントエンドは現在ローカルで実行されており、http://localhost:3000 でアクセスできます。バックエンドをデプロイする場合は、AWS CDKを使用してデプロイし、生成されたAPIエンドポイントをフロントエンドの環境変数に設定することで連携させることができます。
エラーが出ているので直してもらう
01 02 03 04 05 06 07 08 09 10 | Ecmascript file had an error > 1 | import React, { useState, useEffect } from 'react'; | ^^^^^^^^^ 2 | import { Todo, CreateTodoInput, UpdateTodoInput } from '../api/todoApi'; 3 | 4 | interface TodoFormProps { You're importing a component that needs `useEffect`. This React hook only works in a client component. To fix, mark the file (or its parent) with the `"use client"` directive. Learn more: https://nextjs.org/docs/app/api-reference/directives/use-client |
‘use client’の記述がないエラーみたいですね。このエラーを貼り付けてこのエラーを修正して下さい
と投げてみます。
Next.jsのエラーを修正します。エラーメッセージによると、クライアントコンポーネントで使用されるReact Hooksを含むコンポーネントに “use client” ディレクティブが必要です。これを追加して修正しましょう。
…(略)
これでNext.jsのエラーが修正されました。主な修正点は以下の通りです:
クライアントコンポーネントに “use client” ディレクティブを追加
TodoForm.tsx
TodoList.tsx
TodoItem.tsx
型の互換性問題を解決
TodoFormコンポーネントの onSubmit プロパティの型を修正
TodoListコンポーネントで適切な型を使用
メインページでの動的インポート
dynamic を使用してクライアントコンポーネントを正しく読み込む
SSRを無効にして、クライアントサイドのみでレンダリングするように設定
これらの修正により、Next.jsアプリケーションが正しく動作するようになります。フロントエンドは http://localhost:3001 で実行されており、モックAPIを使用してTodoアプリケーションの機能をテストできます。
実際の画面確認
まとめ
対話形式は苦手みたいでしたが、勝手にエラーを見つけて自分で修正していく姿には感動しました!
勝手にコマンドを実行するということもあるのでセキュリティに気を遣う必要はありますが、強力なツールである事は間違いないですね!
ぜひ試してみて下さい!
参考
Claude-3.7-sonnet
https://www.anthropic.com/news/claude-3-7-sonnet