From ai-toolkit
Creates RPC-style API endpoints following layered architecture (Controller → Manager → Repository), including request/response models, controllers, and Retrofit clients for Kotlin/Micronaut backends. Use for new CRUD operations.
npx claudepluginhub c0x12c/ai-toolkit --plugin ai-toolkitThis skill uses the workspace's default tool permissions.
Creates complete RPC-style API endpoints following strict layered architecture patterns.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Designs, implements, and audits WCAG 2.2 AA accessible UIs for Web (ARIA/HTML5), iOS (SwiftUI traits), and Android (Compose semantics). Audits code for compliance gaps.
Creates complete RPC-style API endpoints following strict layered architecture patterns.
Location: 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
)
Location: 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
)
Key: All models in module-client, never in controllers or managers.
Location: 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()
}
}
Key Points:
@ExecuteOn(TaskExecutors.IO) required for suspend functions@QueryValue id: UUID).throwOrValue() to unwrap EitherLocation: 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>
}
Location: 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()
}
}
}
Key Points:
{Domain}Response.from(entity) for conversions (companion object pattern)transaction(db.primary) for writesEither.left() for errors, Either.right() for success!! operatorsLocation: 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)
}
}
Location: 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
}
Location: app/api-application/src/test/kotlin/com/yourcompany/{Domain}ControllerTest.kt
See
testing-patterns.mdfor complete test examples.
./gradlew :app:api-application:test --tests "{Domain}ControllerTest"
./gradlew test
This project uses RPC-style endpoints: @Get for reads, @Post for all mutations, query parameters for all IDs:
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)
Rules from API_RULES.md:
/{id})@QueryValue id: UUID/delete, /restore)HTTP Request → Controller → Manager → Repository → Database
Controller: Thin (just delegation), HTTP annotations, @ExecuteOn(TaskExecutors.IO), @Secured, unwrap Either with .throwOrValue()
Manager: All business logic, returns Either<ClientException, T>, wraps DB operations in transactions, never throws exceptions
Repository: Data access only (already exists)
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()
}
@ExecuteOn(TaskExecutors.IO)@QueryValue for all IDs)!! operatorcompanion object { fun from() }