Custom endpoints - advanced
Request context
Within the context of handleRequest, the following properties are in scope:
| Property | Description | Available |
|---|---|---|
db | The database instance | Always |
body | The body of the request | Always |
userName | The user name of the user making the request | When logged in |
request | The request object | Always |
Any write call to the db will create audit entries for auditable tables, and will be executed in a transaction, if supported by the database layer.
Additionally, the triggerEvent function is available to trigger events from the endpoint:
endpoint<Trade, Trade>(POST, "insert-trade") {
handleRequest {
val trade = db.insert(body).record
triggerEvent("NOTIFY_TRADE_CREATED", trade)
trade
}
}
Endpoint paths
The endpoint by default will take its root from the file name. For example, if the file is called trade-web-handler.kts, all endpoints are prefixed with trade.
Overriding the base path
You can specify a basePath in the webHandlers block:
webHandlers("my-base-path") {
endpoint(GET, "all-trades") {
handleRequest {
db.getBulk(TRADE)
}
}
}
In the example above, the path would be:
- my-base-path/all-trades
Adding additional path levels
You can add extra path segments using the grouping function in this way:
webHandlers("BASE-PATH") {
grouping("trade") {
endpoint(GET, "all-trades") {
handleRequest {
db.getBulk(TRADE)
}
}
endpoint(GET, "big-trades") {
handleRequest {
db.getBulk(TRADE)
.filter { it.quantity!! > 1_000 }
}
}
}
}
In the example above, the paths would be:
- tables/trade/all-trades
- tables/trade/big-trades
Config
The config function can be used to configure endpoints, which are supported on different levels:
webHandlerslevelgroupinglevelendpointlevels
config calls in nested blocks override those in parent blocks.
Example
This is an example of a config block:
config {
requiresAuth = false
maxRecords = 10_000
logLevel = DEBUG
json {
prettyPrint = true
propertyCase = PropertyCase.CAMEL_CASE
}
multiPart {
maxFileSize = 10_000_000
useDisk = true
baseDir = "runtime/router/fileuploadtemp"
minSize = 100_000
}
}
Available config options
config is available within the webHandler block, the grouping block, the endpoint block, and the
multipartEndpoint block.
| Syntax | Description |
|---|---|
requiresAuth | Defines that the endpoint requires authentication |
maxRecord | Defines the maximum number of records returned |
logLevel | Defines the log level |
json { ... } | Defines the JSON configuration |
multiPart { ... } | Defines the multipart configuration |
register(requestParsers) | Registers request parsers |
register(responseComposers) | Registers response composers |
parseRequest<INPUT, TYPE> { ... } | Defines a request parser from INPUT to TYPE |
parseRequest<INPUT, TYPE>(contentType) { ... } | Defines a request parser for a specific content type |
composeResponse<TYPE, OUTPUT> { ... } | Defines a response composer for TYPE to OUTPUT |
composeResponse<TYPE, OUTPUT>(contentType) { ... } | Defines a response composer for TYPE to OUTPUT for a specific content type |
JSON options
| Syntax | Description |
|---|---|
prettyPrint | Defines that JSON should be pretty printed |
propertyCase | Defines the case of JSON properties, either camel case, or snake case |
Multipart options
| Syntax | Description |
|---|---|
maxFileSize | Defines the maximum file size |
useDisk | Defines that files should be written to disk |
baseDir | Defines the base directory for files written to disk |
minSize | Defines the minimum size for files written to disk |
Custom type handling
Use parseRequest and composeResponse to define custom type handling. This is useful if the framework doesn't support the required content type, or if you need
custom handling of the request or response. These blocks are available within the config block, at each level.
To use these, you must provide two types: the input type, and the output type. Optionally, you can add a content type.
Request parsing
When parsing a request, the input type tells the endpoint how to handle the initial parsing of the request. For example, if you want to handle the input as a String, you can do this:
endpoint<Trade, Trade>(RequestType.PUT, "test") {
config {
parseRequest<String, Trade> {
Trade {
tradeId = input
tradeType = "SWAP"
currencyId = "USD"
tradeDate = DateTime.now()
}
}
}
handleRequest {
body
}
}
Here, the input type is String, and the output type is Trade. The parseRequest block takes a lambda that takes the input type, and returns the output type. The output type is then passed to the handleRequest block as the body.
Response composing
When composing a response, the output type tells the endpoint how to handle the final part of the response; this can be any type that the endpoint supports. For example, if you want to produce an endpoint to produce a custom xml, you could do this:
endpoint<String, String>(RequestType.GET, "test") {
config {
composeResponse<Trade, String>(ContentType.APPLICATION_XML) {
"""
<trade>
<tradeId>${response.tradeId}</tradeId>
<tradeType>${response.tradeType}</tradeType>
<currencyId>${response.currencyId}</currencyId>
<tradeDate>${response.tradeDate}</tradeDate>
</trade>
""".trimIndent()
}
}
produces(ContentType.APPLICATION_XML)
handleRequest {
db.get(Trade.ById(body))
}
}
Http status codes
By default, all endpoints return a 200 OK status code.
Default status code
You can override the default response status by setting the status explicitly:
webHandlers {
endpoint<Trade, InsertResult<Trade>>(POST, "insert-trade", status = HttpStatusCode.Created) {
handleRequest {
db.insert(body)
}
}
}
HttpResponseCode annotation
Use the HttpResponseCode annotation to set the status code for a specific class. This can be especially useful with Kotlin sealed classes, where different subclasses return different status codes.
In the example below, if our endpoint returns SealedResponse, we will return a 200 OK status code for AllGood, and a 404 Not Found status code for Missing:
sealed class SealedResponse {
@HttpResponseCode(HttpStatusCode.Ok)
data class AllGood(val motivation: String) : SealedResponse()
@HttpResponseCode(HttpStatusCode.NotFound)
data class Missing(val sadMessage: String) : SealedResponse()
}
Exception handling
Another way to handle different status codes is to handle exceptions. The exceptionHandler block enables you to catch specific exceptions and provide a response, including a specific status code.
In this example, we return status code 406 Not Acceptable for IllegalArgumentException:
webHandlers {
endpoint<Trade, InsertResult<Trade>>(POST, "insert-trade", status = HttpStatusCode.Created) {
handleRequest {
require(trade.date >= DateTime.now()) {
"Trade date cannot be in the past"
}
db.insert(body)
}
exceptionHandler {
exceptionHandler<IllegalArgumentException>(HttpStatusCode.NotAcceptable) {
exception.message ?: "Error handling trade"
}
}
}
}
Request parameters
In addition to the body, endpoints can also take request parameters. These are defined in the endpoint block, and are available in the handleRequest block.
The framework supports the following parameter types:
- query parameter
- path parameter
- header parameter
These can be optional or required. If a required parameter is missing, it will not be matched. If no matching endpoint is found, a 404 Not Found will be returned.
Use the by syntax to define parameters. Note that these variables are only available within the handleRequest block. If they are accessed outside this block, an exception will be thrown.
Query parameters
Here is a simple example of how to define a query parameter:
endpoint(RequestType.GET, "test") {
val name by queryParameter("name")
handleRequest {
"Hello $name"
}
}
Here is an example of how to define an optional query parameter. Optional parameters are always nullable.
endpoint(RequestType.GET, "test") {
val name by optionalQueryParameter("name")
handleRequest {
"Hello ${name ?: "Anonymous"}"
}
}
Path parameters
Path parameters are always required. Here is an example of how to define one:
endpoint(RequestType.GET, "test/{name}") {
val name by pathParameter("name")
handleRequest {
"Hello $name"
}
}
Header parameters
Here is an example of how to define a header parameter:
endpoint(RequestType.GET, "test") {
val name by header("name")
handleRequest {
"Hello $name"
}
}
Here is an example of how to define an optional header parameter. Optional parameters are always nullable.
endpoint(RequestType.GET, "test") {
val name by optionalHeader("name")
handleRequest {
"Hello $name"
}
}
Required values
Headers can also have a set of required values. If the header is present, but the value does not match, then the endpoint will not be matched. Note that the required values are published as part of the OpenAPI specification, unless they are declared a secret.
The below endpoint will only match if the Test-Header header is present with its value set to test:
endpoint(RequestType.GET, "test") {
header("Test-Header", "test")
handleRequest {
"Hello World"
}
}
Secret values
Here is an example of how to define a secret header:
endpoint(RequestType.GET, "test") {
headerSecret("secret-header", "secret-value")
handleRequest {
"OK"
}
}
OpenAPI
Open API support was introduced in version 7.0 of the Genesis platform.
By default, the framework generates a basic OpenAPI specification for all endpoints. This includes the path, and the schemas of the request and response type (if supported). To enable this, use the openapi block to provide additional information, such as descriptions and examples.
We can provide the following information:
descriptionsummaryresponserequestBodyparameters
Description and summary
We can provide a description and summary for our endpoint:
endpoint(RequestType.GET, "test") {
openapi {
description = "A test endpoint"
summary = "This endpoint is available for testing..."
}
// removed for brevity
}
Response
A schema for the response type used in the endpoint is generated automatically. This schema includes support for sealed types. However, the schema can be customised if needed. You can also provide:
- examples
- descriptions
- additional responses
Here is an example:
endpoint<TestData>(RequestType.GET, "test") {
openapi {
response {
description = "A test response"
example(TestData("Hello World", 1))
}
response(HttpStatusCode.NotAcceptable) {
noBody()
}
response(HttpStatusCode.NotFound) {
example("Something went missing")
}
}
// removed for brevity
}
The example above defines the standard response of type TestData. There is also an example and a description. We also define that if the status code is 406 Not Acceptable, then there will be no body. Further, if the status code is 404 Not Found, then the body will be a string.
Request body
As with responses, the request body schema is generated automatically. However, this can also be customised.
Similarly, you can provide a description and example. Providing an example for a request body is very useful for testing the endpoint in the OpenAPI UI. The request is ready to go with the example data, which the user can modify to suit their needs.
endpoint<TestData, String>(RequestType.GET, "test") {
openapi {
requestBody {
description = "A test request"
example(TestData("test", 1))
}
}
// removed for brevity
}
The example above takes TestData as the request body and returns a String. It also provides a description
and example for the request body.
Parameters
By default, the framework describes parameters in the OpenAPI spec. However, you can provide additional information, for example:
endpoint<String>(RequestType.GET, "users") {
openapi {
parameters {
query("userGroup") {
description = "The user group to filter by"
}
}
}
// removed for brevity
}