connekt-script-writer
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, noif/else, no variable assignments outside ofthenoruseCaseblocks. - Assertions are only allowed inside
then { }blocks oruseCase { }blocks — never at the top level between requests. - Never interpolate results from other requests directly into a URL. Use
pathParaminstead, 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. onRequestBuilderfor shared headers), request declarations (val x by GET/POST/...), anduseCaseblocks.
// ✅ 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()
}
Script Structure
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 envreads fromconnekt.env.json— the property name is the lookup keyval response by GET(...)delegates execution and binds the resultthen { ... }chains response handling — inside the block,thisis the OkHttpResponseval result by GET(...) then { expr }captures thethenblock'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 reporting- No code between requests — assertions and logic go inside
thenoruseCaseonly - 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() // don't follow 3xx redirects
noCookies() // exclude cookies from this request
http2() // use HTTP/2 (h2c)
}
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") // 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)
}
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
// 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)
}
}
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)
// Многошаговый бизнес-сценарий — оправданное применение 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
}
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" }
}
// Session cookie is sent automatically
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
thenoruseCaseblocks only. - Never interpolate request results into URLs (
"$host/api/pets/$petId") — always usepathParam("petId", petId)with a{petId}placeholder in the URL. - Top-level extension functions on
RequestBuilderare fine for reusable request configuration (shared headers, auth, etc.). - Never use deprecated functions:
vars,variable<T>(),doRead(),readString(),readInt(),readLong(),readBoolean(). Usedecode<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.
More from amplicode/spring-skills
java-debug
>
14spring-planning
Create structured implementation plan in docs/plans/
12spring-explore
>
12spring-data-jpa
Rules and guidelines for working with Spring Data JPA in the project. ALWAYS use this skill when adding, removing, or modifying JPA entities, repositories, or projections. Trigger on any request that involves changing entity structure, adding new entities, modifying field annotations, updating database mappings, creating or modifying Spring Data repositories, or defining query projections (interfaces, DTOs).
11mapper-creator
>
11dto-creator
>
11