mcpField
A
api-endpoint-creator

api-endpoint-creator

github.com/c0x12c开发109110 次安装19 周前更新
代码仓库

关于 api-endpoint-creator

此 skill 按项目的分层 Kotlin 架构创建 RPC 风格 API 端点,适用于需要新增接口的开发者。它涵盖请求与响应模型、Controller、Manager、Factory、Retrofit 客户端和集成测试,并遵循项目约定。

主要功能

  1. 01生成完整端点结构及测试
  2. 02遵循 Controller-Manager-Repository 分层
  3. 03使用查询参数传递标识符
  4. 04采用 Either 进行错误处理
  5. 05创建 Retrofit 集成客户端

适用场景

  1. 01为领域实体添加 CRUD 端点
  2. 02从头构建 REST API 端点
  3. 03为端点编写集成测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/c0x12c/ai-toolkit --skill api-endpoint-creator

安装后在 Claude Code 中输入/api-endpoint-creator确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

error-handling-guide.md
examples.md
testing-patterns.md

详细介绍

API 端点创建技能

按照严格的分层架构模式创建完整的 RPC 风格 API 端点。

适用场景

  • 从头开始创建新的 REST API 端点
  • 为新的领域实体添加 CRUD 操作
  • 搭建完整技术栈:Controller → Manager → Repository → 测试
  • 需要用于集成测试的 Retrofit 客户端

流程

1. 创建响应/请求模型

位置: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 中,绝不放在 controllers 或 managers 中。

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()
  }
}

关键点:

  • suspend 函数必须使用 @ExecuteOn(TaskExecutors.IO)
  • 所有标识符都使用查询参数(@QueryValue id: UUID)
  • 方法保持精简——仅委托给 manager
  • 使用 .throwOrValue() 解包 Either
  • 只注入 Manager(绝不注入 Repository)
  • 禁止内联 data class
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) 进行转换(companion object 模式)
  • 写操作使用 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

交互风格

  • 始终生成完整技术栈:模型、controller、manager、factory、客户端和测试
  • 遵循项目的确切模式——不走捷径,也不采用自创的替代方案
  • 所有 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>,将数据库操作包装在事务中,绝不抛出异常

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(绝不抛出异常)
  • 使用事务包装数据库操作
  • 不使用 !! 运算符
  • 响应模型位于 module-client 中,并包含 companion object { fun from() }
  • Controller 中不使用内联 data class
  • 集成测试使用 Retrofit 客户端
  • 所有测试均通过

评论

支持 Markdown