1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS KiroのSpec駆動開発って何が凄いの?【Part2 実装編】

1
Last updated at Posted at 2026-02-26

はじめに

Part1 では、KiroのSpec駆動開発の概要と、requirements.md / design.md / tasks.md が自動生成される流れを紹介しました。

この記事では、そのspecをもとに実際にAWSサーバーレスAPIを実装した過程を紹介します。

特に注目したいのはここです:

  • specの通りにコードが生成された部分(設計意図が守られた部分)

作ったもの

認証付きメモ管理API(サーバーレス)

技術スタック:
  バックエンド: API Gateway + Lambda (TypeScript) + DynamoDB + Cognito
  インフラ:     AWS CDK (TypeScript)
  フロントエンド: React + Vite (S3 + CloudFront)

エンドポイント一覧:

メソッド パス 認証 機能
POST /auth/signup 不要 ユーザー登録
POST /auth/login 不要 ログイン・トークン取得
POST /memos 必要 メモ作成
GET /memos 必要 メモ一覧取得
GET /memos/{id} 必要 メモ1件取得
DELETE /memos/{id} 必要 メモ削除

ディレクトリ構成

.
├── bin/
│   └── memo-api.ts          # CDKエントリーポイント
├── lib/
│   ├── memo-api-stack.ts    # バックエンドCDKスタック
│   └── frontend-stack.ts    # フロントエンドCDKスタック
├── lambda/
│   ├── auth/
│   │   ├── signup.ts
│   │   └── login.ts
│   ├── memos/
│   │   ├── create-memo.ts
│   │   ├── get-memos.ts
│   │   ├── get-memo.ts
│   │   └── delete-memo.ts
│   └── shared/
│       ├── types.ts
│       ├── errors.ts
│       └── error-handler.ts
└── frontend/
    └── src/                 # React SPA

CDKスタックの実装

specの通りに生成された部分:Cognito Authorizerの設定

前編で紹介した design.md には、こう書かれていました。

### Cognito Authorizer設定
- Authorizationヘッダーから Bearer <token> 形式でトークンを取得
- Cognitoでトークンを検証
- 検証成功時、ユーザーIDをLambdaのイベントコンテキストに追加

実際に生成されたCDKコードです:

// Cognito Authorizer
const authorizer = new apigateway.CognitoUserPoolsAuthorizer(
  this,
  'MemoApiAuthorizer',
  {
    cognitoUserPools: [this.userPool],
    identitySource: 'method.request.header.Authorization',
  }
);

// メモ系エンドポイントにAuthorizerを適用
memos.addMethod('POST', new apigateway.LambdaIntegration(createMemoFunction), {
  authorizer,
  authorizationType: apigateway.AuthorizationType.COGNITO,
});

memos.addMethod('GET', new apigateway.LambdaIntegration(getMemosFunction), {
  authorizer,
  authorizationType: apigateway.AuthorizationType.COGNITO,
});

specで「Cognito Authorizerで認証する」と明示されていたため、Lambda側でJWT検証するコードは一切生成されませんでした。

DynamoDBテーブルとGSIの設定

// DynamoDB Table
this.memosTable = new dynamodb.Table(this, 'MemosTable', {
  tableName: 'Memos',
  partitionKey: {
    name: 'memoId',
    type: dynamodb.AttributeType.STRING,
  },
  billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
  removalPolicy: cdk.RemovalPolicy.DESTROY,
});

// GSI: userId でクエリするためのインデックス
this.memosTable.addGlobalSecondaryIndex({
  indexName: 'UserIdIndex',
  partitionKey: {
    name: 'userId',
    type: dynamodb.AttributeType.STRING,
  },
  sortKey: {
    name: 'createdAt',
    type: dynamodb.AttributeType.STRING,
  },
});

design.md で「ユーザーのメモ一覧を取得するためのGSIが必要」と設計されていたため、GSIも正しく定義されました。


Lambda実装

共通エラークラス(shared/errors.ts)

エラーハンドリングも specで設計されており、カスタムエラークラスが整備されました。

export class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ValidationError';
    Object.setPrototypeOf(this, ValidationError.prototype);
  }
}

export class ForbiddenError extends Error { ... }
export class NotFoundError extends Error { ... }
export class UnauthorizedError extends Error { ... }
export class ConflictError extends Error { ... }

共通エラーハンドラ(shared/error-handler.ts)

エラークラスをHTTPステータスコードに変換する関数も生成されています。

export function handleError(error: unknown): APIGatewayProxyResult {
  if (error instanceof ValidationError) {
    return createErrorResponse(400, 'VALIDATION_ERROR', error.message);
  }
  if (error instanceof UnauthorizedError) {
    return createErrorResponse(401, 'UNAUTHORIZED', error.message);
  }
  if (error instanceof ForbiddenError) {
    return createErrorResponse(403, 'FORBIDDEN', error.message);
  }
  if (error instanceof NotFoundError) {
    return createErrorResponse(404, 'NOT_FOUND', error.message);
  }
  if (error instanceof ConflictError) {
    return createErrorResponse(409, 'CONFLICT', error.message);
  }
  return createErrorResponse(500, 'INTERNAL_SERVER_ERROR', 'An unexpected error occurred');
}

Signup Lambda(lambda/auth/signup.ts)

export async function handler(
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> {
  try {
    if (!event.body) {
      throw new ValidationError('リクエストボディが必要です');
    }

    const request: SignupRequest = JSON.parse(event.body);

    // バリデーション
    validateEmail(request.email);
    validatePassword(request.password);

    // Cognito SignUp
    const command = new SignUpCommand({
      ClientId: USER_POOL_CLIENT_ID,
      Username: request.email,
      Password: request.password,
      UserAttributes: [{ Name: 'email', Value: request.email }],
    });

    const result = await cognitoClient.send(command);

    return createSuccessResponse(200, {
      userId: result.UserSub!,
      email: request.email,
    });
  } catch (error: any) {
    // CognitoエラーをカスタムエラーにマッピングW
    if (error.name === 'UsernameExistsException') {
      return handleError(new ConflictError('このメールアドレスは既に登録されています'));
    }
    return handleError(error);
  }
}

Create Memo Lambda(lambda/memos/create-memo.ts)

メモ作成時は、Cognito Authorizerが検証済みのトークンから userId を取り出します。

function getUserIdFromEvent(event: APIGatewayProxyEvent): string {
  const userId = event.requestContext?.authorizer?.claims?.sub;
  if (!userId) {
    throw new ValidationError('ユーザーIDが取得できません');
  }
  return userId;
}

export async function handler(event: APIGatewayProxyEvent) {
  const request: CreateMemoRequest = JSON.parse(event.body!);
  validateContent(request.content);

  const userId = getUserIdFromEvent(event);  // ← Authorizerのclaimsから取得
  const memoId = uuidv4();
  const createdAt = new Date().toISOString();

  await docClient.send(new PutCommand({
    TableName: MEMOS_TABLE_NAME,
    Item: { memoId, userId, content: request.content.trim(), createdAt },
  }));

  return createSuccessResponse(200, { memoId, userId, content, createdAt });
}

Get Memos Lambda(lambda/memos/get-memos.ts)

メモ一覧の取得は、GSI UserIdIndex を使って自分のメモだけを取得します。

export async function handler(event: APIGatewayProxyEvent) {
  const userId = getUserIdFromEvent(event);

  const result = await docClient.send(new QueryCommand({
    TableName: MEMOS_TABLE_NAME,
    IndexName: 'UserIdIndex',
    KeyConditionExpression: 'userId = :userId',
    ExpressionAttributeValues: { ':userId': userId },
    ScanIndexForward: false, // createdAt 降順
  }));

  return createSuccessResponse(200, { memos: result.Items || [] });
}

Delete Memo Lambda(lambda/memos/delete-memo.ts)

削除前に所有者チェックを行います。他ユーザーのメモは削除できません。

export async function handler(event: APIGatewayProxyEvent) {
  const memoId = event.pathParameters?.id;
  const userId = getUserIdFromEvent(event);

  // メモを取得して所有者確認
  const getResult = await docClient.send(new GetCommand({
    TableName: MEMOS_TABLE_NAME,
    Key: { memoId },
  }));

  if (!getResult.Item) {
    throw new NotFoundError('メモが見つかりません');
  }

  if (getResult.Item.userId !== userId) {
    throw new ForbiddenError('このメモを削除する権限がありません');  // 403
  }

  await docClient.send(new DeleteCommand({
    TableName: MEMOS_TABLE_NAME,
    Key: { memoId },
  }));

  return createSuccessResponse(200, { message: 'メモが正常に削除されました' });
}

デプロイとAPIの動作確認

# デプロイ
npm install
cdk bootstrap  # 初回のみ
cdk deploy

# デプロイ完了後に出力される情報
# ApiUrl: https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod
# UserPoolId: ap-northeast-1_xxxxxxxxx
# UserPoolClientId: xxxxxxxxxxxxxxxxxxxxxxxxxx

動作確認:

# ユーザー登録
curl -X POST https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/auth/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"Test1234!"}'

# ログイン → idTokenを取得
curl -X POST https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"Test1234!"}'

# メモ作成(認証必要)
curl -X POST https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/memos \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <idToken>" \
  -d '{"title":"テストメモ","content":"Kiroで作ったAPIのテストです"}'

# メモ一覧取得
curl https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/memos \
  -H "Authorization: Bearer <idToken>"

すべて設計通りに動作しました。


中編まとめ

項目 結果
Cognito Authorizerの設定 ✅ specの通りに生成
DynamoDB GSIの設計 ✅ specの通りに生成
Lambda間の共通エラーハンドリング ✅ specの通りに生成
所有者チェック(403)ロジック ✅ specの通りに生成

大枠の設計はspecの通りに出てくる。

というのが正直な感想です。

次の 後編 では、vibeコーディングとSpec駆動を改めて比較し、どう使い分けるかを考察します。


関連リンク

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?