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で閉じておきます。
Cursor Yoloモード

例の如くToDoアプリ作らせてみた

NextJS環境構築

空のディレクトリを用意して、Cursorで開きます。
そしてCOMPOSERで以下のプロンプトを入力します。左下でモデルをclaude-3.7-sonnetを指定し、右下でagentモードを選んでsubmitします。
prompt

フロントエンド環境構築

まず、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' });
  }
};

Lint 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ファイルを作成して、アプリケーションの概要と使い方を説明します。

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

Cursorのagentモードに関する記述(Yoloモードにも言及しています)

https://docs.cursor.com/agent