api-endpoint-creator について
プロジェクトのレイヤード Kotlin アーキテクチャに沿って、RPC 形式の API エンドポイントを作成するスキルです。リクエスト・レスポンスモデルから Controller、Manager、Factory、Retrofit クライアント、統合テストまでを扱い、プロジェクト固有の規約に従います。
主な機能
- 01エンドポイント一式とテストの生成
- 02Controller・Manager・Repository の階層化
- 03識別子にクエリパラメーターを使用
- 04Either によるエラー処理
- 05Retrofit 統合クライアントの作成
活用シーン
- 01ドメインエンティティの CRUD 追加
- 02REST API エンドポイントの新規作成
- 03エンドポイントの統合テスト作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ c0x12c/ ai-toolkit --skill api-endpoint-creatorインストール後、Claude Code で/api-endpoint-creatorを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
error-handling-guide.mdexamples.mdtesting-patterns.md詳細
API エンドポイント作成スキル
厳格なレイヤードアーキテクチャのパターンに従って、RPC スタイルの API エンドポイントを完全に作成します。
使用するタイミング
- REST API エンドポイントをゼロから新規作成するとき
- 新しいドメインエンティティに CRUD 操作を追加するとき
- Controller → Manager → Repository → Tests のフルスタックをセットアップするとき
- 統合テスト用の Retrofit クライアントが必要なとき
手順
1. Response/Request モデルを作成する
場所:app/module-client/src/main/kotlin/com/yourcompany/client/response/{domain}/
package com.yourcompany.client.response.{domain}
import com.yourcompany.postgresql.entity.{Domain}Entity
import java.time.Instant
import java.util.UUID
data class {Domain}Response(
val id: UUID,
val name: String,
val status: String,
val createdAt: Instant,
val updatedAt: Instant?
) {
companion object {
fun from(entity: {Domain}Entity): {Domain}Response = {Domain}Response(
id = entity.id,
name = entity.name,
status = entity.status,
createdAt = entity.createdAt,
updatedAt = entity.updatedAt
)
}
}
data class {Domain}ListResponse(
val items: List<{Domain}Response>,
val total: Int,
val page: Int,
val limit: Int,
val hasMore: Boolean
)
場所:app/module-client/src/main/kotlin/com/yourcompany/client/request/{domain}/
package com.yourcompany.client.request.{domain}
data class Create{Domain}Request(
val name: String,
val description: String? = null
)
data class Update{Domain}Request(
val name: String? = null,
val description: String? = null,
val status: String? = null
)
重要:すべてのモデルは module-client に配置し、コントローラーやマネージャーには配置しません。
2. Controller を作成する
場所:app/api-application/src/main/kotlin/com/yourcompany/controller/{Domain}Controller.kt
package com.yourcompany.controller
import com.yourcompany.{domain}.contract.{Domain}Manager
import com.yourcompany.client.request.{domain}.Create{Domain}Request
import com.yourcompany.client.request.{domain}.Update{Domain}Request
import com.yourcompany.client.response.{domain}.{Domain}Response
import com.yourcompany.client.response.{domain}.{Domain}ListResponse
import com.yourcompany.exception.throwOrValue
import io.micronaut.http.annotation.*
import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn
import io.micronaut.security.annotation.Secured
import io.micronaut.validation.Validated
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import java.util.UUID
@ExecuteOn(TaskExecutors.IO)
@Validated
@Controller("/api/v1/{domain}")
@Tag(name = "{Domain}", description = "{Domain} API")
@Secured(SecurityRule.IS_AUTHENTICATED)
class {Domain}Controller(
private val {domain}Manager: {Domain}Manager
) {
@Get("/{domain}s")
suspend fun list(
@QueryValue page: Int?,
@QueryValue limit: Int?,
@QueryValue status: String?
): {Domain}ListResponse {
return {domain}Manager.list(
page = page ?: 1,
limit = limit ?: 20,
status = status
).throwOrValue()
}
@Get("/{domain}")
suspend fun getById(
@QueryValue id: UUID
): {Domain}Response {
return {domain}Manager.byId(id).throwOrValue()
}
@Post("/{domain}")
suspend fun create(
@Valid @Body request: Create{Domain}Request
): {Domain}Response {
return {domain}Manager.create(request).throwOrValue()
}
@Post("/{domain}/update")
suspend fun update(
@QueryValue id: UUID,
@Valid @Body request: Update{Domain}Request
): {Domain}Response {
return {domain}Manager.update(id, request).throwOrValue()
}
@Post("/{domain}/delete")
suspend fun delete(
@QueryValue id: UUID
): Boolean {
return {domain}Manager.deleteById(id).throwOrValue()
}
}
重要なポイント:
- サスペンド関数には
@ExecuteOn(TaskExecutors.IO)が必要です - すべての識別子にクエリパラメーターを使用します(
@QueryValue id: UUID) - メソッドは薄く保ち、マネージャーに委譲するだけにします
.throwOrValue()で Either を展開します- Manager のみを注入します(Repository は決して注入しません)
- インラインのデータクラスは使用しません
3. Manager インターフェースを作成する
場所:app/module-{domain}/module-api/src/main/kotlin/com/yourcompany/{domain}/contract/{Domain}Manager.kt
package com.yourcompany.{domain}.contract
import arrow.core.Either
import com.yourcompany.client.request.{domain}.Create{Domain}Request
import com.yourcompany.client.request.{domain}.Update{Domain}Request
import com.yourcompany.client.response.{domain}.{Domain}Response
import com.yourcompany.client.response.{domain}.{Domain}ListResponse
import com.yourcompany.exception.ClientException
import java.util.UUID
interface {Domain}Manager {
suspend fun list(
page: Int,
limit: Int,
status: String?
): Either<ClientException, {Domain}ListResponse>
suspend fun byId(id: UUID): Either<ClientException, {Domain}Response>
suspend fun create(
request: Create{Domain}Request
): Either<ClientException, {Domain}Response>
suspend fun update(
id: UUID,
request: Update{Domain}Request
): Either<ClientException, {Domain}Response>
suspend fun deleteById(id: UUID): Either<ClientException, Boolean>
}
4. Manager の実装を作成する
場所:app/module-{domain}/module-impl/src/main/kotlin/com/yourcompany/{domain}/impl/Default{Domain}Manager.kt
package com.yourcompany.{domain}.impl
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.yourcompany.database.DatabaseContext
import com.yourcompany.{domain}.contract.{Domain}Manager
import com.yourcompany.client.request.{domain}.Create{Domain}Request
import com.yourcompany.client.request.{domain}.Update{Domain}Request
import com.yourcompany.client.response.{domain}.{Domain}Response
import com.yourcompany.client.response.{domain}.{Domain}ListResponse
import com.yourcompany.exception.ClientError
import com.yourcompany.exception.ClientException
import com.yourcompany.postgresql.entity.{Domain}Entity
import com.yourcompany.postgresql.repository.{Domain}Repository
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.UUID
class Default{Domain}Manager(
private val {domain}Repository: {Domain}Repository,
private val db: DatabaseContext
) : {Domain}Manager {
override suspend fun byId(id: UUID): Either<ClientException, {Domain}Response> {
val entity = {domain}Repository.byId(id)
?: return ClientError.{DOMAIN}_NOT_FOUND.asException().left()
return {Domain}Response.from(entity).right()
}
override suspend fun create(
request: Create{Domain}Request
): Either<ClientException, {Domain}Response> {
val entity = {Domain}Entity(
name = request.name,
description = request.description
)
val inserted = transaction(db.primary) {
{domain}Repository.insert(entity)
}
return {Domain}Response.from(inserted).right()
}
override suspend fun update(
id: UUID,
request: Update{Domain}Request
): Either<ClientException, {Domain}Response> {
val existing = {domain}Repository.byId(id)
?: return ClientError.{DOMAIN}_NOT_FOUND.asException().left()
val updated = transaction(db.primary) {
{domain}Repository.update(
id = id,
name = request.name,
description = request.description,
status = request.status
)
} ?: return ClientError.{DOMAIN}_NOT_FOUND.asException().left()
return {Domain}Response.from(updated).right()
}
override suspend fun deleteById(id: UUID): Either<ClientException, Boolean> {
val deleted = transaction(db.primary) {
{domain}Repository.deleteById(id)
}
return if (deleted != null) {
true.right()
} else {
ClientError.{DOMAIN}_NOT_FOUND.asException().left()
}
}
}
重要なポイント:
- 変換には
{Domain}Response.from(entity)を使用します(コンパニオンオブジェクトのパターン) - 書き込みには
transaction(db.primary)を使用します - エラーには
Either.left()、成功にはEither.right()を返します !!演算子は使用しません
5. Factory Bean を登録する
場所:app/module-{domain}/module-impl/src/main/kotlin/com/yourcompany/runtime/factory/{Domain}ManagerFactory.kt
package com.yourcompany.runtime.factory
import com.yourcompany.database.DatabaseContext
import com.yourcompany.{domain}.impl.Default{Domain}Manager
import com.yourcompany.{domain}.contract.{Domain}Manager
import com.yourcompany.postgresql.repository.{Domain}Repository
import io.micronaut.context.annotation.Factory
import jakarta.inject.Singleton
@Factory
class {Domain}ManagerFactory {
@Singleton
fun provide{Domain}Manager(
{domain}Repository: {Domain}Repository,
db: DatabaseContext
): {Domain}Manager {
return Default{Domain}Manager({domain}Repository, db)
}
}
6. Retrofit クライアントを作成する
場所:app/module-client/src/main/kotlin/com/yourcompany/client/{Domain}Client.kt
package com.yourcompany.client
import com.yourcompany.client.request.{domain}.Create{Domain}Request
import com.yourcompany.client.request.{domain}.Update{Domain}Request
import com.yourcompany.client.response.{domain}.{Domain}Response
import com.yourcompany.client.response.{domain}.{Domain}ListResponse
import retrofit2.http.*
import java.util.UUID
interface {Domain}Client {
@GET("/api/v1/{domain}s")
suspend fun list(
@Header("Authorization") authorization: String,
@Query("page") page: Int? = null,
@Query("limit") limit: Int? = null,
@Query("status") status: String? = null
): {Domain}ListResponse
@GET("/api/v1/{domain}")
suspend fun getById(
@Header("Authorization") authorization: String,
@Query("id") id: UUID
): {Domain}Response
@POST("/api/v1/{domain}")
suspend fun create(
@Header("Authorization") authorization: String,
@Body request: Create{Domain}Request
): {Domain}Response
@POST("/api/v1/{domain}/update")
suspend fun update(
@Header("Authorization") authorization: String,
@Query("id") id: UUID,
@Body request: Update{Domain}Request
): {Domain}Response
@POST("/api/v1/{domain}/delete")
suspend fun delete(
@Header("Authorization") authorization: String,
@Query("id") id: UUID
): Boolean
}
7. 統合テストを作成する
場所:app/api-application/src/test/kotlin/com/yourcompany/{Domain}ControllerTest.kt
完全なテスト例については、testing-patterns.md を参照してください。
8. テストを実行する
./gradlew :app:api-application:test --tests "{Domain}ControllerTest"
./gradlew test
対話スタイル
- 常に、モデル、コントローラー、マネージャー、ファクトリー、クライアント、テストを含むフルスタックを生成します
- プロジェクトの正確なパターンに従い、近道や独自の代替案は使いません
- すべての ID にクエリパラメーターを使用し、パスパラメーターは決して使いません
- コンテキストから明らかでない場合は、開始前に対象のドメインエンティティを尋ねます
ルール
RPC スタイル API(変更には POST、クエリパラメーターのみ)
このプロジェクトでは、RPC スタイルのエンドポイントを使用します。読み取りには @Get、すべての変更には @Post を使用し、すべての ID にはクエリパラメーターを使用します。
GET /api/v1/employees # List employees (plural)
GET /api/v1/employee # Get one employee (?id=xxx)
POST /api/v1/employee # Create employee
POST /api/v1/employee/update # Update employee
POST /api/v1/employee/delete # Delete employee (soft)
API_RULES.md のルール:
- パスパラメーター(
/{id})は決して使用しません - クエリパラメーターを使用します:
@QueryValue id: UUID - 単一リソースには単数形、コレクションには複数形を使用します
- アクションには動詞のサブパスを使用します(
/delete、/restore)
レイヤードアーキテクチャ
HTTP Request → Controller → Manager → Repository → Database
Controller:薄く保ち(委譲のみ)、HTTP アノテーション、@ExecuteOn(TaskExecutors.IO)、@Secured を使用し、.throwOrValue() で Either を展開します
Manager:すべてのビジネスロジックを担い、Either<ClientException, T> を返し、DB 操作をトランザクションで囲み、例外をスローしません
Repository:データアクセスのみ(すでに存在します)
!! 演算子は使用しない
val employee = employeeRepository.byId(id)
?: return ClientError.EMPLOYEE_NOT_FOUND.asException().left()
エラーハンドリングのパターン
見つからない場合
val entity = repository.byId(id)
?: return ClientError.{DOMAIN}_NOT_FOUND.asException().left()
すでに存在する場合
val existing = repository.byEmail(email)
if (existing != null) {
return ClientError.EMAIL_ALREADY_IN_USE.asException().left()
}
出力
- Controller は薄く保たれています(委譲のみ)
- Controller に
@ExecuteOn(TaskExecutors.IO)が付いています - パスパラメーターはありません(すべての ID に
@QueryValueを使用します) - Manager は Either を返します(例外をスローしません)
- DB 操作はトランザクションで囲まれています
!!演算子は使用していません- Response モデルは module-client に配置され、
companion object { fun from() }を備えています - コントローラーにインラインのデータクラスはありません
- 統合テストでは Retrofit クライアントを使用します
- すべてのテストが成功します