| name | connekt-script-writer |
| description | Write `.connekt.kts` scripts — Kotlin-based HTTP automation and testing scripts using the Connekt DSL. Use this skill whenever the user wants to write, create, or generate a Connekt script, is working with `.connekt.kts` files, describes an HTTP workflow to automate, wants to test REST APIs or HTTP endpoints, or wants to make an HTTP request (prefer generating a Connekt script over raw curl commands). Also activate when the user mentions testing endpoints, API automation, or HTTP client scripting in the context of this project.
|
Connekt Script Writer
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.
Execution Model
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:
- No imperative code between requests. No
println, no if/else, no variable assignments outside of then or useCase blocks.
- Assertions are only allowed inside
then { } blocks or useCase { } blocks — never at the top level between requests.
- Never interpolate results from other requests directly into a URL. Use
pathParam instead, so the runner can resolve values at execution time.
- Allowed at top level:
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.
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)
}
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()
}
Script Structure
A .connekt.kts file follows this conventional ordering:
@file:Import("shared_auth.connekt.kts")
@file:DependsOn("com.example:my-lib:1.0")
import org.assertj.core.api.Assertions.assertThat
val host: String by env
val apiKey: String by env
data class Pet(val id: Long, val name: String, val status: String)
configureClient {
insecure()
}
val auth by oauth(
authorizeEndpoint = "...",
clientId = "...",
clientSecret = "...",
scope = "openid",
tokenEndpoint = "...",
redirectUri = "http://localhost:8080/callback"
)
val pets by GET("$host/api/pets") then {
decode<List<Pet>>("$.content")
}
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 key
val response by GET(...) delegates execution and binds the result
then { ... } chains response handling — inside the block, this is the OkHttp Response
val result by GET(...) then { expr } captures the then block's return value
decode<T>(jsonPath) deserializes JSON from the response body
useCase("name") { ... } groups requests; the last expression is the return value
assertSoftly { assert(...) } collects all assertion failures before reporting
- No code between requests — assertions and logic go inside
then or useCase only
- Pass request results via
pathParam, never interpolate them into the URL string
DSL Reference
HTTP Methods
GET("$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")
}
Request Configuration
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()
noCookies()
http2()
}
Request Bodies
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())
}
Authentication
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"
)
)
Response Handling
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")
}
val pet by GET("$host/api/pet/1") then {
decode<Pet>()
}
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)
}
Use Cases
Когда использовать useCase
useCase применяется только если сценарий включает несколько запросов с передачей результата между ними (например, создать ресурс, затем проверить его существование).
- Один HTTP-запрос (даже с
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) }
}
Environment Variables
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.
Assertions
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)
}
}
Script Imports and Dependencies
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")
SSL/TLS and Client Configuration
configureClient {
insecure()
addX509Certificate(java.io.File("certs/my-ca.crt"))
addKeyStore(java.io.File("certs/truststore.jks"), "changeit")
readTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
}
GET("$host/api/slow-endpoint") {
configureClient {
readTimeout(120, java.util.concurrent.TimeUnit.SECONDS)
}
}
Common Patterns
Simple GET with assertions
val host: String by env
GET("$host/api/health") then {
assert(code == 200) { "Health check failed with status $code" }
println(body!!.string())
}
POST with JSON body and response extraction
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>()
}
CRUD workflow
val host: String by env
data class Pet(val id: Long, val name: String, val status: String)
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
}
OAuth2 authenticated requests
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")
}
Form login then API call
val host: String by env
POST("$host/login") {
formData {
field("username", "admin")
field("password", "admin")
}
} then {
assert(code == 200 || code == 302) { "Login failed" }
}
val dashboard by GET("$host/api/dashboard") then {
assert(code == 200)
decode<Map<String, Any>>()
}
File upload (multipart)
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>>()
}
Paginated loop
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()
}
Multi-script reuse with @file:Import
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)
}
Soft assertion response validation
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)
}
}
Important Notes
- По умолчанию генерируй одиночный запрос — без
useCase. useCase оправдан только когда сценарий явно содержит несколько шагов с передачей данных между запросами. Один запрос (с then или без) никогда не оборачивается в useCase. Это правило имеет наивысший приоритет.
- The script registers requests — it does not execute them imperatively. No code between requests at the top level. Assertions and logic belong inside
then or useCase blocks only.
- Never interpolate request results into URLs (
"$host/api/pets/$petId") — always use pathParam("petId", petId) with a {petId} placeholder in the URL.
- Top-level extension functions on
RequestBuilder are fine for reusable request configuration (shared headers, auth, etc.).
- Never use deprecated functions:
vars, variable<T>(), doRead(), readString(), readInt(), readLong(), readBoolean(). Use decode<T>() for all response extraction.
- Cookies are managed automatically in a session jar — no manual cookie handling needed unless you use
noCookies().
- By default, OkHttp follows 3xx redirects. Use
noRedirect() to inspect redirect responses.
insecure() disables all SSL verification — use only for local development.