Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
amplicode avatar

Connekt Script Writer

  • 155 installs
  • 105 repo stars
  • Updated July 27, 2026
  • amplicode/spring-skills

connekt-script-writer is an agent skill that writes .connekt.kts HTTP automation and REST API test scripts using the Connekt DSL.

About

connekt-script-writer helps solo builders turn described HTTP flows into maintainable Connekt Kotlin scripts. Instead of fragile curl one-liners, you get .connekt.kts files that declare requests declaratively, pull configuration from connekt.env.json, and express checks inside then or useCase blocks where the runner expects them. The skill encodes Connekt’s execution model explicitly: scripts register requests rather than running linearly, which prevents common mistakes like top-level assertions or logic sandwiched between calls. Use it when automating REST checks, chaining authenticated calls, or documenting how an API should behave for future you or CI. It fits backend-heavy SaaS and integration work where Kotlin is already on the classpath via Spring-skills context, and it keeps secrets out of committed scripts by convention.

  • Top-level Connekt DSL in .connekt.kts without a main() boilerplate
  • Reads base URLs and secrets from connekt.env.json via val x: String by env
  • Runner executes selected requests only—no imperative println/if between requests
  • Assertions restricted to then { } or useCase { } blocks
  • Preferred over raw curl when the user describes repeatable HTTP workflows

Connekt Script Writer by the numbers

  • 155 all-time installs (skills.sh)
  • Ranked #2,420 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
  • Security screen: LOW risk (skills.sh audit)
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/amplicode/spring-skills --skill connekt-script-writer

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs155
repo stars105
Security audit3 / 3 scanners passed
Last updatedJuly 27, 2026
Repositoryamplicode/spring-skills

What it does

Generate.connekt.kts Kotlin scripts that call REST endpoints with env-based secrets instead of one-off curl commands.

Who is it for?

Best when you're automating or asserting REST workflows in a Kotlin/Connekt project with connekt.env.json.

Skip if: Pure browser E2E UI tests or languages outside the Connekt.kts script model.

When should I use this skill?

User wants to write, create, or generate a Connekt script, work with .connekt.kts, automate HTTP workflows, or test REST endpoints (prefer over raw curl).

What you get

You receive a valid .connekt.kts script that loads env values and registers requests with assertions in the right blocks.

  • .connekt.kts script file
  • Env-referenced HTTP request definitions with optional then/useCase assertions

Files

SKILL.mdMarkdownGitHub ↗

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.
// ✅ 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 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()   // 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 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.

Related skills

How it compares

Structured Connekt scripts with env indirection, not ad-hoc curl pasted into chat.

FAQ

Who is connekt-script-writer for?

Developers on Amplicode/Spring-skills projects who test or automate HTTP APIs with Connekt rather than manual curl.

When should I use connekt-script-writer?

Use it in Ship/testing when verifying endpoints before release, and in Build/integrations when you first encode a repeatable HTTP workflow as.connekt.kts.

Is connekt-script-writer safe to install?

Review the Security Audits panel on this Prism page; scripts should load secrets from env files, not hardcoded literals.

Backend & APIsintegrationstestingbackend

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.