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.