Simple REST Endpoint
The following tutorial uses the example project codebase. Make sure you have the repository cloned so you can follow along and run the tests.
This example will create a REST endpoint on the test server that echos back the value it receives from a POST request. In a real-world use case, the flow would most likely perform other operations (like writing to a database) before sending a response back.
Creating the Flow
Open the Kotlin file located at main/kotlin/flowexamples/e01/E01SimpleRestFlow.kt
. This is where the flow specification is written, along with the other information needed to deploy a REST endpoint.
The REST Resource
The flow-server creates REST endpoints by interpreting OpenAPI specifications. These specs can be uploaded to the flow-server in JSON or YAML syntax. This example uses a JSON file. Open the file located at main/resources/echo-open-api-v1.json
and look over its contents. In particular, note the following property:
"/echo": {
"post": {
"operationId": "sayHi",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ToBeEchoedDto"
}
}
}
}
}
}
This defines a POST route on the path /echo
. When this flow is deployed to the server, /echo
becomes an actionable endpoint that can receive data and further process it in the flow.
You can paste the contents of the JSON file into Swagger’s online editor to see a rendered version of the specification in Swagger UI. The Swagger editor is also a great tool for building OpenAPI specs from scratch. |
The OpenAPI specification is ultimately sent to the server as a string. In the E01SimpleRestFlow
object, we use a custom fromClasspath()
function to read the contents of the echo-open-api-v1.json
file and save it to a property on the object, as the following code shows:
object E01SimpleRestFlow {
val simpleRestOpenApiDefinition = fromClasspath("/echo-open-api-v1.json")
...
}
We then add another object property using the following newResourceRevisionKey
builder function:
val simpleRestResourceKey = newResourceRevisionKey {
ownerId = OWNER_ID
type = "OpenAPIv3"
id = "simple-rest-api"
revision = "latest"
}
This sets up the metadata that the server will need to properly reference the OpenAPI spec. The OpenAPI spec isn’t directly attached to the flow. Instead, an identifier like OpenAPIv3:simple-rest-api:latest
is used to look up the correct resource. We’ll see this come into play in the next step.
The Flow Specification
The third property on the E01SimpleRestFlow
object is the flow specification, which uses a flowConfig
builder with the following high-level properties:
val simpleRestSpec = flowConfig {
id = "simple-rest"
description = "Simple REST Flow"
ownerId = OWNER_ID
exchangePattern = RequestResponse
...
}
We set the exchangePattern
to RequestResponse
, because we intend for the client to receive a response from the REST endpoint as opposed to being a "fire and forget" type of data transfer. This means we will need at least two processors in the flow: one to act as the inbound endpoint and another to define what to send back.
Processors are added to a flowConfig
as additional properties. The following restApi
processor signifies that the inbound endpoint for the flow is a REST request:
restApi {
id = "echo-api"
apiSpecId = simpleRestResourceKey.toResourceIdentifier()
}
Note how apiSpecId
is declared as the identifer from the simpleRestResourceKey
object, which will become the string "OpenAPIv3:simple-rest-api:latest"
.
The restApi
processor will pass along the data it receives to the next processor in the flow, which happens to be the following map
processor:
map {
id = "echo-response-creator"
mapSpec = """
{
"message" : #input.payload.value
}
""".trimIndent()
}
We’ll look at the map
processor in more detail in a later tutorial.
For now, just know that it uses a query language to format JSON documents.
In the mapping, we have access to the results of the previous processor on the #input.payload
property.
value
is not a predefined property, but we intend to make POST requests with a body of {"value":"testValue"}
, so #input.payload.value
retrieves the incoming string.
The mapped response will then look like the following:
{
"message" : "testValue"
}
Testing the Flow
Open the Kotlin class located at test/kotlin/flowexamples/e01/E01SimpleRestFlowTest.kt
. This is a single test that will deploy the flow and make a POST request to its endpoint. The overall class structure looks like the following code:
class E01SimpleRestFlowTest : ConcurrentTestBase() {
@Test
fun `E01 GIVEN deployed echo flow WHEN sending a value THEN the value is echoed back`(ctx: ConcurrentTestContext) {
...
}
}
By inheriting the ConcurrentTestBase
class, we can enable running tests concurrently. Even though this example only has one test, it is still a good idea to start with concurrency in mind. We also add a ConcurrentTestContext
parameter to the @Test
function so our test has a way to interact with the concurrent testing framework. Flows and other resources will be added to this context object.
First, we create a Resource
instance with the OpenAPI specification and metadata, as the following code demonstrates:
val openApiResource = Resource(key = simpleRestResourceKey, content = simpleRestOpenApiDefinition)
We then add the resource and flow to the ConcurrentTestContext
object with the following line:
ctx.addFlowTestConfig { resource(openApiResource); flow(simpleRestSpec) }
The flowTest()
function starts the test server and deploys the resources and flows from the context. flowTest()
is a higher-order function, so the following block of code is executed after deployment:
flowTest(ctx) {
val inputValue = "testValue"
val responseMessage = restApiEndpoint(simpleRestSpec)
.path("echo")
.request()
.basicAuth()
.post(json(SimpleValue(inputValue)), SimpleMessage::class.java)
assertThat(responseMessage.message).isEqualTo(inputValue)
}
The test itself uses the SDK’s restApiEndpoint()
function to create a WebTarget
instance that will make an authenticated POST request to the /echo
route.
We use the SimpleValue
data class to format the request body as {"value":"testValue"}
and the SimpleMessage
data class to specify how to interpret the response.
Note that SimpleMessage
will match the message
JSON property that was defined in the map
processor.
Lastly, we use assertThat()
from the AssertJ library to verify that the response from the endpoint is the same as the request that was sent.
Remember to run the test so you can see that it passes! The IntelliJ terminal will also print the following information that will convey the resource and flow being deployed to the server and then undeployed after the test finishes:
10:32:08.499 [INFO] ResourceDeployer - Deploying resource ResourceRevisionKey(ownerId=exampleOwner, type=OpenAPIv3, id=simple-rest-api, revision=latest). 10:32:08.622 [INFO] FlowTestManager$ManagedFlow - Deploying 'simple-rest-1e6125'. 10:32:10.247 [INFO] FlowTestManager$ManagedFlow - Undeploying 'simple-rest-1e6125'.
Flows deployed on the test server append a six-digit hex string to the end of the original ID. For instance, simple-rest becomes simple-rest-1e6125 . This helps with concurrent testing, as different instances of the same flow can be deployed from parallel tests.
|
From here, you can experiment with different request types and paths, change the data that is expected back, or proceed to the next tutorial on outbound REST requests.