|
| 1 | +# API Development Patterns |
| 2 | + |
| 3 | +Load this context when creating or modifying API endpoints. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Response Wrapper |
| 8 | + |
| 9 | +ALL API responses MUST use `BaseResponse<T>`: |
| 10 | + |
| 11 | +```kotlin |
| 12 | +data class BaseResponse<T>( |
| 13 | + val resultCode: String = ResultCode.SUCCESS.code, |
| 14 | + val message: String = ResultCode.SUCCESS.message, |
| 15 | + val success: Boolean = true, |
| 16 | + val data: T? = null, |
| 17 | +) |
| 18 | +``` |
| 19 | + |
| 20 | +**Usage in Controller:** |
| 21 | +```kotlin |
| 22 | +@GetMapping |
| 23 | +fun getAllFolders(): BaseResponse<GetAllFolderResponse> { |
| 24 | + val result = getFoldersUseCase.execute(command) |
| 25 | + val response = resultConverter.toResponse(result) |
| 26 | + return BaseResponse(data = response) |
| 27 | +} |
| 28 | +``` |
| 29 | + |
| 30 | +Reference: `src/main/kotlin/com/yapp2app/common/api/dto/BaseResponse.kt` |
| 31 | + |
| 32 | +--- |
| 33 | + |
| 34 | +## Data Flow Pattern |
| 35 | + |
| 36 | +``` |
| 37 | +Request → Controller → Converter → Command → UseCase → Result → Converter → Response |
| 38 | +``` |
| 39 | + |
| 40 | +### Complete Example |
| 41 | + |
| 42 | +```kotlin |
| 43 | +@RestController |
| 44 | +@RequestMapping("/api/folders") |
| 45 | +class FolderController( |
| 46 | + private val createFolderUseCase: CreateFolderUseCase, |
| 47 | + private val commandConverter: FolderCommandConverter, |
| 48 | + private val resultConverter: FolderResultConverter, |
| 49 | +) { |
| 50 | + @PostMapping |
| 51 | + fun createFolder( |
| 52 | + @AuthenticationPrincipal(expression = "id") userId: Long, |
| 53 | + @Valid @RequestBody request: CreateFolderRequest, |
| 54 | + ): BaseResponse<CreateFolderResponse> { |
| 55 | + // 1. Convert request to command |
| 56 | + val command = commandConverter.toCreateFolderCommand(request, userId) |
| 57 | + |
| 58 | + // 2. Execute use case |
| 59 | + val result = createFolderUseCase.execute(command) |
| 60 | + |
| 61 | + // 3. Convert result to response |
| 62 | + val response = resultConverter.toCreateFolderResponse(result) |
| 63 | + |
| 64 | + // 4. Wrap in BaseResponse |
| 65 | + return BaseResponse(data = response) |
| 66 | + } |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +--- |
| 71 | + |
| 72 | +## Exception Handling |
| 73 | + |
| 74 | +Use `BusinessException` with `ResultCode`: |
| 75 | + |
| 76 | +```kotlin |
| 77 | +// Throwing exceptions |
| 78 | +throw BusinessException(ResultCode.CONFLICT_FOLDER) |
| 79 | +throw BusinessException(ResultCode.NOT_FOUND) |
| 80 | + |
| 81 | +// Available result codes |
| 82 | +enum class ResultCode(val code: String, val message: String) { |
| 83 | + SUCCESS("D-0", "OK"), |
| 84 | + ERROR("D-99", "ERROR"), |
| 85 | + INVALID_PARAMETER("D-01", "Invalid input"), |
| 86 | + ALREADY_SIGNUP("D-02", "Already registered"), |
| 87 | + NOT_FOUND_USER("D-03", "User not found"), |
| 88 | + NOT_FOUND("D-04", "Data not found"), |
| 89 | + ALREADY_REQUEST("D-05", "Already processed"), |
| 90 | + CONFLICT_FOLDER("D-06", "Folder already exists"), |
| 91 | + // Token errors |
| 92 | + EXPIRED_TOKEN_ERROR("D-997", "Token expired"), |
| 93 | + INVALID_TOKEN_ERROR("D-998", "Invalid token"), |
| 94 | + SECURITY_ERROR("D-999", "Authentication failed"), |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +Key files: |
| 99 | +- `src/main/kotlin/com/yapp2app/common/exception/BusinessException.kt` |
| 100 | +- `src/main/kotlin/com/yapp2app/common/api/dto/ResultCode.kt` |
| 101 | +- `src/main/kotlin/com/yapp2app/common/exception/handler/ExceptionHandler.kt` |
| 102 | + |
| 103 | +--- |
| 104 | + |
| 105 | +## Swagger Documentation |
| 106 | + |
| 107 | +Every endpoint MUST include `@Operation`: |
| 108 | + |
| 109 | +```kotlin |
| 110 | +@Operation( |
| 111 | + summary = "Create folder", // Short description |
| 112 | + description = "Creates a new photo folder for the user." // Detailed for frontend devs |
| 113 | +) |
| 114 | +@PostMapping |
| 115 | +fun createFolder(...): BaseResponse<CreateFolderResponse> |
| 116 | +``` |
| 117 | + |
| 118 | +### Multi-Step Workflow Documentation |
| 119 | + |
| 120 | +For endpoints that are part of a workflow, document the full flow: |
| 121 | + |
| 122 | +```kotlin |
| 123 | +@Operation( |
| 124 | + summary = "Register photo image", |
| 125 | + description = """ |
| 126 | + Photo upload workflow: |
| 127 | + 1. Call GET /api/media/presigned-url → Get S3 upload URL |
| 128 | + 2. Client uploads file directly to S3 |
| 129 | + 3. Call this API to register the photo metadata |
| 130 | +
|
| 131 | + The imageKey from step 1 must be passed to this endpoint. |
| 132 | + """ |
| 133 | +) |
| 134 | +``` |
| 135 | + |
| 136 | +### Security Annotation |
| 137 | + |
| 138 | +Protected endpoints use `@RequiresSecurity`: |
| 139 | + |
| 140 | +```kotlin |
| 141 | +@RequiresSecurity // Marks endpoint as requiring authentication in Swagger |
| 142 | +@Tag(name = "folder", description = "Folder APIs") |
| 143 | +@RestController |
| 144 | +@RequestMapping("/api/folders") |
| 145 | +class FolderController |
| 146 | +``` |
| 147 | + |
| 148 | +Reference: `src/main/kotlin/com/yapp2app/common/api/document/SwaggerConfig.kt` |
| 149 | + |
| 150 | +--- |
| 151 | + |
| 152 | +## Request Validation |
| 153 | + |
| 154 | +Use Jakarta validation annotations: |
| 155 | + |
| 156 | +```kotlin |
| 157 | +data class CreateFolderRequest( |
| 158 | + @field:NotBlank(message = "Folder name is required") |
| 159 | + @field:Size(max = 50, message = "Folder name must be 50 characters or less") |
| 160 | + val name: String, |
| 161 | +) |
| 162 | +``` |
| 163 | + |
| 164 | +Validation errors are automatically handled and return: |
| 165 | +```json |
| 166 | +{ |
| 167 | + "resultCode": "D-01", |
| 168 | + "message": "Folder name is required", |
| 169 | + "success": false, |
| 170 | + "errors": [ |
| 171 | + { "field": "name", "message": "Folder name is required" } |
| 172 | + ] |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +--- |
| 177 | + |
| 178 | +## Authentication |
| 179 | + |
| 180 | +Get current user ID from JWT: |
| 181 | + |
| 182 | +```kotlin |
| 183 | +@PostMapping |
| 184 | +fun createFolder( |
| 185 | + @AuthenticationPrincipal(expression = "id") userId: Long, // Extracts user ID from token |
| 186 | + @RequestBody request: CreateFolderRequest, |
| 187 | +): BaseResponse<CreateFolderResponse> |
| 188 | +``` |
| 189 | + |
| 190 | +--- |
| 191 | + |
| 192 | +## Checklist for New Endpoints |
| 193 | + |
| 194 | +- [ ] Use `BaseResponse<T>` wrapper |
| 195 | +- [ ] Create Request/Response DTOs in `api/dto/` |
| 196 | +- [ ] Create Command in `application/command/` |
| 197 | +- [ ] Create Result in `application/result/` |
| 198 | +- [ ] Create Converters in `api/converter/` |
| 199 | +- [ ] Add `@Operation` with summary and description |
| 200 | +- [ ] Add `@RequiresSecurity` if authentication required |
| 201 | +- [ ] Add validation annotations to request DTOs |
| 202 | +- [ ] Write E2E tests (see `@.claude/docs/TESTING.md`) |
0 commit comments