Generates .connekt.kts Kotlin scripts using Connekt DSL for HTTP automation, REST API testing, and endpoint workflows. Reads connekt.env.json for env vars; follows request-then-assert structure.
npx claudepluginhub amplicode/spring-skills --plugin spring-toolsThis skill uses the workspace's default tool permissions.
Connekt is an HTTP client driven by Kotlin scripts. Scripts use the `.connekt.kts` extension and have the full Connekt DSL available at the top level — no boilerplate, no main function, just declarations and requests.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Connekt is an HTTP client driven by Kotlin scripts. Scripts use the .connekt.kts extension and have the full Connekt DSL available at the top level — no boilerplate, no main function, just declarations and requests.
When generating scripts, always read from connekt.env.json for base URLs and secrets using val x: String by env rather than hardcoding values. Save scripts with the .connekt.kts extension.
A script only registers requests — the runner decides which request to execute. The script is NOT run top-to-bottom like a regular program. This means:
println, no if/else, no variable assignments outside of then or useCase blocks.then { } blocks or useCase { } blocks — never at the top level between requests.pathParam instead, so the runner can resolve values at execution time.val x by env, data class, configureClient, val x by oauth(...), extension functions (e.g. on RequestBuilder for shared headers), request declarations (val x by GET/POST/...), and useCase blocks.// ✅ CORRECT — pass result via pathParam
val petId by POST("$host/api/pets") {
contentType("application/json")
body("""{"name": "Fido"}""")
} then {
decode<Long>("$.id")
}
GET("$host/api/pets/{petId}") {
pathParam("petId", petId)
} then {
assert(code == 200)
}
// ❌ WRONG — never interpolate request results in URL
GET("$host/api/pets/$petId") then {
assert(code == 200)
}
Extension functions on RequestBuilder are a good way to apply shared configuration:
fun RequestBuilder.withApiKey() {
header("X-Api-Key", apiKey)
header("X-Request-Id", java.util.UUID.randomUUID().toString())
}
GET("$host/api/pets") {
withApiKey()
}
A .connekt.kts file follows this conventional ordering:
// 1. File-level annotations (imports, dependencies)
@file:Import("shared_auth.connekt.kts")
@file:DependsOn("com.example:my-lib:1.0")
// 2. Third-party imports (if needed)
import org.assertj.core.api.Assertions.assertThat
// 3. Environment variables from connekt.env.json
val host: String by env
val apiKey: String by env
// 4. Data classes for typed deserialization
data class Pet(val id: Long, val name: String, val status: String)
// 5. Global client configuration
configureClient {
insecure() // for local dev only
}
// 6. OAuth setup (if needed)
val auth by oauth(
authorizeEndpoint = "...",
clientId = "...",
clientSecret = "...",
scope = "openid",
tokenEndpoint = "...",
redirectUri = "http://localhost:8080/callback"
)
// 7. HTTP requests
val pets by GET("$host/api/pets") then {
decode<List<Pet>>("$.content")
}
// 8. Use cases for grouping related requests
val result by useCase("Create and verify") {
val created by POST("$host/api/pets") {
contentType("application/json")
body("""{"name": "Fido"}""")
} then {
decode<Pet>()
}
created
}
Key conventions:
val x by env reads from connekt.env.json — the property name is the lookup keyval response by GET(...) delegates execution and binds the resultthen { ... } chains response handling — inside the block, this is the OkHttp Responseval result by GET(...) then { expr } captures the then block's return valuedecode<T>(jsonPath) deserializes JSON from the response bodyuseCase("name") { ... } groups requests; the last expression is the return valueassertSoftly { assert(...) } collects all assertion failures before reportingthen or useCase onlypathParam, never interpolate them into the URL stringGET("$host/api/resource")
POST("$host/api/resource")
PUT("$host/api/resource")
PATCH("$host/api/resource")
DELETE("$host/api/resource")
HEAD("$host/api/resource")
OPTIONS("$host/api/resource")
TRACE("$host/api/resource")
Each method accepts an optional name parameter and a configuration lambda:
GET("$host/api/pets", name = "List all pets") {
header("Accept", "application/json")
queryParam("status", "available")
}
Headers:
GET("$host/api/resource") {
header("X-Api-Key", apiKey)
headers("Accept" to "application/json", "X-Request-Id" to "abc123")
contentType("application/json")
accept("application/json")
}
Query parameters:
GET("$host/api/pets") {
queryParam("status", "available")
queryParam("limit", 20)
queryParams("sort" to "name", "order" to "asc")
}
Path parameters (use {name} placeholders in the URL):
DELETE("$host/api/owners/{ownerId}/pets/{petId}") {
pathParam("ownerId", 1)
pathParam("petId", 5)
}
Request options:
GET("$host/old-url") {
noRedirect() // don't follow 3xx redirects
noCookies() // exclude cookies from this request
http2() // use HTTP/2 (h2c)
}
JSON body:
POST("$host/api/pets") {
contentType("application/json")
body("""{"name": "Fido", "species": "dog"}""")
}
Form data (Content-Type is set automatically):
POST("$host/api/login") {
formData {
field("username", "alice")
field("password", "secret")
}
}
Multipart:
POST("$host/api/upload") {
multipart {
part(name = "metadata", contentType = "application/json") {
body("""{"description": "profile picture"}""")
}
file(name = "photo", fileName = "avatar.jpg", file = java.io.File("/tmp/avatar.jpg"))
}
}
Byte array:
POST("$host/api/upload") {
header("Content-Type", "application/octet-stream")
body(java.io.File("/tmp/data.bin").readBytes())
}
Basic and Bearer:
GET("$host/api/resource") { basicAuth("user", "pass") }
GET("$host/api/resource") { bearerAuth(myToken) }
OAuth2 (authorization code flow):
val auth by oauth(
authorizeEndpoint = "$authHost/oauth/authorize",
clientId = clientId,
clientSecret = clientSecret,
scope = "openid profile",
tokenEndpoint = "$authHost/oauth/token",
redirectUri = "http://localhost:8080/callback"
)
GET("$host/api/protected") {
bearerAuth(auth.accessToken)
}
OAuth2 opens a browser for login and starts a local callback server automatically. The auth object provides accessToken and refreshToken.
Keycloak shorthand:
val auth by oauth(
KeycloakOAuthParameters(
serverBaseUrl = keycloakHost,
realm = "my-realm",
protocol = "openid-connect",
clientId = "my-client",
clientSecret = "secret",
scope = "openid",
callbackPort = 8080,
callbackPath = "/callback"
)
)
Inside a then block, this is the OkHttp Response — you have code, body, header("Name"), etc.
Validate and extract:
GET("$host/api/pets") then {
assert(code == 200) { "Expected 200 but got $code" }
val text = body!!.string()
println(text)
}
Typed deserialization with decode<T>():
val pets by GET("$host/api/pets") then {
decode<List<Pet>>("$.content") // JSONPath extraction
}
val pet by GET("$host/api/pet/1") then {
decode<Pet>() // root object (default when no JSONPath given)
}
Raw JSONPath access:
GET("$host/api/stats") then {
val ctx = jsonPath()
val count = ctx.decode<Int>("$.totalCount")
val names = ctx.decode<List<String>>("$.items[*].name")
}
Chaining requests — pass results from one request to the next via pathParam:
val petId by POST("$host/api/pets") {
contentType("application/json")
body("""{"name": "Fido"}""")
} then {
decode<Long>("$.id")
}
GET("$host/api/pets/{petId}") {
pathParam("petId", petId)
} then {
assert(code == 200)
}
useCase применяется только если сценарий включает несколько запросов с передачей результата между ними (например, создать ресурс, затем проверить его существование).then-блоком) никогда не оборачивается в useCase.useCase?» — и ждать ответа перед генерацией кода.Single HTTP request — standalone with then, never wrapped in useCase:
val createdPet by POST("$host/api/pets") {
contentType("application/json")
body("""{"name": "Rex"}""")
} then {
decode<Pet>()
}
Group multiple related requests with useCase. The last expression is the return value.
val payment by useCase("Place order and pay") {
val order by POST("$host/api/orders") {
contentType("application/json")
body("""{"productId": 42, "quantity": 1}""")
} then {
decode<Order>()
}
val payment by POST("$host/api/payments") {
contentType("application/json")
body("""{"orderId": ${order.id}, "amount": ${order.totalPrice}}""")
} then {
decode<Payment>()
}
payment
}
Anonymous use cases (no name) work the same way:
useCase {
GET("$host/foo") then { assert(code == 200) }
GET("$host/bar") then { assert(code == 200) }
}
Read from connekt.env.json using property delegation. The property name is the lookup key:
val host: String by env
val port: Int by env
The connekt.env.json file:
{
"env": {
"host": "http://localhost:8080",
"port": 8080
}
}
Supported types: String, Int, Long, Double, Boolean. Missing keys throw an error.
Kotlin assert (with Power Assert diagnostics when --kotlin-power-assert is used):
GET("$host/api/pets") then {
assert(code == 200) { "Expected 200 but got $code" }
}
AssertJ (requires import, deprecated, do not use in new scripts):
import org.assertj.core.api.Assertions.assertThat
GET("$host/api/pets") then {
assertThat(code).isEqualTo(200)
val pets = decode<List<Pet>>("$.content")
assertThat(pets).isNotEmpty
}
Soft assertions — collect all failures before reporting:
GET("$host/api/users/1") then {
val user = decode<User>()
assertSoftly {
assert(user.name == "Alice")
assert(user.email.contains("@"))
assert(user.age > 0)
assert(user.active)
}
}
Import another script (paths relative to the importing script):
@file:Import("shared_auth.connekt.kts")
@file:Import("utils.connekt.kts")
Imported top-level declarations (vals, functions, classes) become available. Transitive imports work.
External Maven dependencies:
@file:DependsOn("com.example:my-lib:1.0.0")
// Global client config
configureClient {
insecure() // disable SSL verification (dev only!)
addX509Certificate(java.io.File("certs/my-ca.crt"))
addKeyStore(java.io.File("certs/truststore.jks"), "changeit")
readTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
}
// Per-request override
GET("$host/api/slow-endpoint") {
configureClient {
readTimeout(120, java.util.concurrent.TimeUnit.SECONDS)
}
}
val host: String by env
GET("$host/api/health") then {
assert(code == 200) { "Health check failed with status $code" }
println(body!!.string())
}
val host: String by env
data class Pet(val id: Long, val name: String, val status: String)
val created by POST("$host/api/pets") {
contentType("application/json")
body("""{"name": "Fido", "status": "available"}""")
} then {
assert(code == 201) { "Expected 201 but got $code" }
decode<Pet>()
}
val host: String by env
data class Pet(val id: Long, val name: String, val status: String)
// Многошаговый бизнес-сценарий — оправданное применение useCase
val result by useCase("Pet CRUD") {
val pet by POST("$host/api/pets") {
contentType("application/json")
body("""{"name": "Fido", "status": "available"}""")
} then {
assert(code == 201)
decode<Pet>()
}
GET("$host/api/pets/{petId}") {
pathParam("petId", pet.id)
} then {
assert(code == 200)
assert(decode<Pet>().name == "Fido")
}
val updated by PUT("$host/api/pets/{petId}") {
pathParam("petId", pet.id)
contentType("application/json")
body("""{"name": "Rex", "status": "available"}""")
} then {
assert(code == 200)
decode<Pet>()
}
assert(updated.name == "Rex")
DELETE("$host/api/pets/{petId}") {
pathParam("petId", pet.id)
} then { assert(code == 204) }
GET("$host/api/pets/{petId}") {
pathParam("petId", pet.id)
} then { assert(code == 404) }
pet
}
val host: String by env
val authHost: String by env
val clientId: String by env
val clientSecret: String by env
val auth by oauth(
authorizeEndpoint = "$authHost/realms/my-realm/protocol/openid-connect/auth",
clientId = clientId,
clientSecret = clientSecret,
scope = "openid",
tokenEndpoint = "$authHost/realms/my-realm/protocol/openid-connect/token",
redirectUri = "http://localhost:8080/callback"
)
val owners by GET("$host/api/owners") {
bearerAuth(auth.accessToken)
queryParam("lastNameContains", "smith")
} then {
assert(code == 200)
decode<List<String>>("$.content[*].name")
}
val host: String by env
POST("$host/login") {
formData {
field("username", "admin")
field("password", "admin")
}
} then {
assert(code == 200 || code == 302) { "Login failed" }
}
// Session cookie is sent automatically
val dashboard by GET("$host/api/dashboard") then {
assert(code == 200)
decode<Map<String, Any>>()
}
val host: String by env
val uploadResult by POST("$host/api/files/upload") {
multipart {
part(name = "metadata", contentType = "application/json") {
body("""{"description": "Monthly report", "category": "reports"}""")
}
file(name = "document", fileName = "report.pdf", file = java.io.File("/tmp/report.pdf"))
}
} then {
assert(code == 201) { "Upload failed with status $code" }
decode<Map<String, Any>>()
}
val host: String by env
data class Event(val id: String, val summary: String)
data class EventsPage(val items: List<Event>, val nextPageToken: String?)
val allEvents by useCase("Fetch all events") {
val events = mutableListOf<Event>()
var nextPageToken: String? = null
do {
nextPageToken = GET("$host/api/events") {
bearerAuth("my-token")
queryParam("maxResults", 50)
if (nextPageToken != null) {
queryParam("pageToken", nextPageToken!!)
}
} then {
val page = decode<EventsPage>()
events.addAll(page.items)
page.nextPageToken
}
} while (nextPageToken != null)
events.toList()
}
auth_setup.connekt.kts (shared):
val host: String by env
val clientId: String by env
val clientSecret: String by env
val auth by oauth(
authorizeEndpoint = "$host/oauth/authorize",
clientId = clientId,
clientSecret = clientSecret,
scope = "openid",
tokenEndpoint = "$host/oauth/token",
redirectUri = "http://localhost:8080/callback"
)
api_tests.connekt.kts (imports the above):
@file:Import("auth_setup.connekt.kts")
GET("$host/api/protected") {
bearerAuth(auth.accessToken)
} then {
assert(code == 200)
}
val host: String by env
data class User(val name: String, val email: String, val age: Int, val active: Boolean)
GET("$host/api/users/1") then {
assert(code == 200) { "Expected 200 but got $code" }
val user = decode<User>()
assertSoftly {
assert(user.name == "Alice")
assert(user.email.contains("@"))
assert(user.age > 0)
assert(user.active)
}
}
useCase. useCase оправдан только когда сценарий явно содержит несколько шагов с передачей данных между запросами. Один запрос (с then или без) никогда не оборачивается в useCase. Это правило имеет наивысший приоритет.then or useCase blocks only."$host/api/pets/$petId") — always use pathParam("petId", petId) with a {petId} placeholder in the URL.RequestBuilder are fine for reusable request configuration (shared headers, auth, etc.).vars, variable<T>(), doRead(), readString(), readInt(), readLong(), readBoolean(). Use decode<T>() for all response extraction.noCookies().noRedirect() to inspect redirect responses.insecure() disables all SSL verification — use only for local development.