A dynamic endpoint server that loads REST and SOAP endpoints from external JAR files at runtime. Perfect for creating mock servers and test fixtures.
See the docs folder for detailed documentation:
- Examples Overview - Guide to the example applications
- Testing with Testcontainers - Using Spektr as a mock service in tests
- Docker Setup - Running Spektr in Docker
- Dynamic endpoint loading - Load endpoints from JAR files without restarting
- Hot reload - Add or update endpoint JARs and reload via API
- REST and SOAP support - Define both REST and SOAP endpoints in the same module
- Protocol toggles - Enable or disable REST and SOAP independently via configuration
- DSL-based configuration - Define endpoints using a simple Kotlin DSL
- ServiceLoader discovery - Automatically discovers
EndpointModuleimplementations
Spektr is composed of several repositories that work together:
| Project | Description |
|---|---|
| spektr | The server application (this repo) |
| spektr-dsl | Kotlin DSL for defining REST and SOAP endpoints |
| spektr-gradle-plugin | Gradle plugin for building and versioning endpoint JARs |
| spektr-test | Testing utilities including Testcontainers support and the @WithSpektr annotation |
All Spektr artifacts are published to a custom Maven repository. Add it to your settings.gradle.kts:
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
maven {
url = uri("https://open-reliquary.nyc3.cdn.digitaloceanspaces.com")
}
}
}And to your build.gradle.kts repositories block for library dependencies:
repositories {
mavenCentral()
maven {
url = uri("https://open-reliquary.nyc3.cdn.digitaloceanspaces.com")
}
}./gradlew :app:bootJarUse the spektr-gradle-plugin to simplify building endpoint JARs:
plugins {
id("org.khorum.oss.plugins.open.spektr") version "1.0.13"
}
dependencies {
compileOnly("org.khorum.oss.spektr:spektr-dsl:1.0.7")
}
spektr {
apiProvider {
jarBaseName = "my-endpoints"
}
}Then implement the EndpointModule interface from the spektr-dsl:
package com.example.endpoints
import org.khorum.oss.spektr.dsl.*
class MyEndpoints : EndpointModule {
override fun EndpointRegistry.configure() {
get("/api/hello/{name}") { request ->
val name = request.pathVariables["name"]
returnBody(mapOf("message" to "Hello, $name!"))
}
post("/api/users") { request ->
// Use DynamicResponse for custom status codes
DynamicResponse(status = 201, body = mapOf("created" to true))
}
delete("/api/users/{id}") { request ->
returnStatus(204)
}
}
override fun SoapEndpointRegistry.configureSoap() {
operation("/ws/greeting", "SayHello") { request ->
SoapResponse(
body = """
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<SayHelloResponse>
<message>Hello from SOAP!</message>
</SayHelloResponse>
</soap:Body>
</soap:Envelope>
""".trimIndent()
)
}
}
}Register it in META-INF/services/org.khorum.oss.spektr.dsl.EndpointModule:
com.example.endpoints.MyEndpoints
java -jar app/build/libs/app.jar --endpoint-jars.dir=./my-jars| Property | Default | Description |
|---|---|---|
endpoint-jars.dir |
./endpoint-jars |
Directory containing endpoint JAR files |
spektr.rest.enabled |
true |
Enable or disable REST endpoint loading |
spektr.soap.enabled |
true |
Enable or disable SOAP endpoint loading |
| Variable | Description |
|---|---|
ENDPOINT_JARS_DIR |
Override the endpoint JARs directory |
SPEKTR_REST_ENABLED |
Enable/disable REST support (true/false) |
SPEKTR_SOAP_ENABLED |
Enable/disable SOAP support (true/false) |
JAVA_OPTS |
JVM options (when using Docker) |
REST only (disable SOAP):
spektr:
soap:
enabled: falseSOAP only (disable REST):
spektr:
rest:
enabled: falseBoth enabled (default):
spektr:
rest:
enabled: true
soap:
enabled: trueYou can inject additional configuration using Spring Boot's standard mechanisms:
Environment variable:
SPRING_CONFIG_IMPORT=optional:file:./my-config.yamlCommand line:
java -jar app.jar --spring.config.import=optional:file:./my-config.yamlMultiple config files:
java -jar app.jar --spring.config.additional-location=file:./custom.yamldocker build -t spektr .docker run -p 8080:8080 spektrdocker run -p 8080:8080 \
-v /path/to/your/jars:/app/endpoint-jars \
spektrdocker run -p 8080:8080 \
-e SPRING_CONFIG_IMPORT=optional:file:/app/config/custom.yaml \
-v /my/config:/app/config \
-v /my/jars:/app/endpoint-jars \
spektrdocker run -p 8080:8080 \
-e SPEKTR_SOAP_ENABLED=false \
spektrdocker run -p 8080:8080 \
-e JAVA_OPTS="-Xmx512m -Xms256m" \
spektrReload all endpoints from the configured JAR directory:
curl -X POST http://localhost:8080/admin/endpoints/reloadResponse:
{
"endpointsLoaded": 5,
"soapEndpointsLoaded": 3,
"jarsProcessed": 2,
"reloadTimeMs": 42
}Upload a new JAR file and reload endpoints:
curl -X POST http://localhost:8080/admin/endpoints/upload \
-F "jar=@my-endpoints.jar"get("/path") { request -> returnBody(data) }
post("/path") { request -> returnBody(data) }
put("/path") { request -> returnBody(data) }
patch("/path") { request -> returnBody(data) }
delete("/path") { request -> returnStatus(204) }
options("/path") { request -> returnBody(data) }get("/users/{id}") { request ->
val id = request.pathVariables["id"]
returnBody(mapOf("id" to id))
}get("/users") { request ->
val active = request.queryParams["active"]?.firstOrNull()?.toBoolean()
val filtered = if (active == true) users.filter { it.active } else users
returnBody(filtered)
}request.pathVariables // Map<String, String> - path parameters
request.queryParams // Map<String, List<String>> - query string
request.headers // Map<String, List<String>> - HTTP headers
request.body // String? - request bodySimple helpers for common responses:
// Return JSON body with 200 status
returnBody(mapOf("key" to "value"))
// Return specific status code (no body)
returnStatus(204)For more control, use DynamicResponse directly:
DynamicResponse(
status = 201, // HTTP status code
body = mapOf("key" to "value"), // Response body (auto-serialized to JSON)
headers = mapOf("X-Custom" to "value") // Response headers
)errorOn(
method = HttpMethod.GET,
path = "/api/error",
status = 500,
body = mapOf("error" to "Something went wrong")
)For parsing JSON request bodies, use Jackson with the Kotlin module:
import tools.jackson.databind.json.JsonMapper
import tools.jackson.module.kotlin.KotlinModule
import tools.jackson.module.kotlin.readValue
private val mapper = JsonMapper.builder()
.addModule(KotlinModule.Builder().build())
.build()
data class CreateUserRequest(val name: String, val email: String)
post("/api/users") { request ->
val body = requireNotNull(request.body) { "Request body required" }
val user: CreateUserRequest = mapper.readValue(body)
// ... create user
returnBody(user)
}Override configureSoap() in your EndpointModule to define SOAP endpoints:
class MySoapEndpoints : EndpointModule {
override fun EndpointRegistry.configure() {
// REST endpoints (can be empty if SOAP-only)
}
override fun SoapEndpointRegistry.configureSoap() {
operation("/ws/myservice", "MyAction") { request ->
SoapResponse(body = "<MyResponse>...</MyResponse>")
}
}
}operation(path, soapAction) { request -> SoapResponse(...) }path- the URL path for the SOAP endpoint (e.g.,/ws/myservice)soapAction- the SOAPAction header value to matchrequest- contains headers, soapAction, and the raw XML body
request.headers // Map<String, List<String>> - HTTP headers
request.soapAction // String - the SOAPAction value
request.body // String? - raw SOAP XML bodySoapResponse(
status = 200, // HTTP status code (default 200)
body = "<soap:Envelope>...</soap:Envelope>", // XML response body
headers = mapOf("X-Custom" to "value") // Response headers
)Use soapFault() to register a fault response for a specific action:
soapFault(
path = "/ws/myservice",
soapAction = "BadAction",
faultCode = "soap:Client",
faultString = "Operation not supported"
)SOAP endpoints are invoked via POST with the SOAPAction header and text/xml content type:
curl -X POST http://localhost:8080/ws/myservice \
-H "Content-Type: text/xml" \
-H 'SOAPAction: "MyAction"' \
-d '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<MyRequest><param>value</param></MyRequest>
</soap:Body>
</soap:Envelope>'A single EndpointModule can define both REST and SOAP endpoints:
class MixedEndpoints : EndpointModule {
override fun EndpointRegistry.configure() {
get("/api/status") { _ ->
DynamicResponse(body = mapOf("status" to "ok"))
}
}
override fun SoapEndpointRegistry.configureSoap() {
operation("/ws/status", "GetStatus") { _ ->
SoapResponse(
body = """
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetStatusResponse><status>ok</status></GetStatusResponse>
</soap:Body>
</soap:Envelope>
""".trimIndent()
)
}
}
}Spektr logs incoming requests, matched endpoints, and responses. Configure log levels in your application.yaml:
logging:
level:
org.khorum.oss.spektr.service: INFOExample output:
Matched endpoint: GET /api/house/{id} -> /api/house/123
Path variables: {id=123}
Request body: 45 bytes
Returning response: status=200, body type=House
logging:
level:
org.khorum.oss.spektr.service: DEBUGThe spektr-test library provides Testcontainers support for using Spektr as a mock service in your integration tests:
dependencies {
testImplementation("org.khorum.oss.spektr:spektr-test:1.0.7")
}@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@WithSpektr(
endpointJarsPath = "path/to/endpoint-jars",
properties = ["external-service.base-url"]
)
class MyIntegrationTest @Autowired constructor(
private val webTestClient: WebTestClient
) {
@Test
fun `calls mocked external service`() {
webTestClient.get()
.uri("/my-endpoint")
.exchange()
.expectStatus().isOk
}
}See Testing with Testcontainers for full documentation.
./gradlew test./gradlew :app:bootRun --args='--spring.profiles.active=test'MIT