Kiit uses a somewhat new, yet familiar paradigm to building out APIs by enriching normal Kotlin methods and making them easily discoverable and accessible across a range of hosts. This will resemble an RPC type approach, but contains some support and concepts from REST. More specifically, APIs in Kiit can be hosted and made available as Web/HTTP APIs, on the CLI, or called from requests from queues or files for automation purposes. Under the hood, Kiit simply leverages existing HTTP servers ( currently Ktor ), to host, discover, manage, and access Kiit APIs. Our CLI also supports the ability to host Kiit APIs. This specific approach to API development in Kiit is referred to as Universal APIs.
slatekit new api -name="SampleAPI" -package="mycompany.apps"
A high-level diagram of the concepts in this component
Goal | Description |
1. Accessable | Write once and have APIs hosted and/or run anywhere ( e.g. Web, CLI, from File/Queue sources ) |
2. Discoverable | Folder like heirarchical and drill-down discovery of APIS using a 3 part routing format { area / api / action } |
3. Natural | Allow normal methods to be easily turned to API actions with strong types, automatic docs and accurate Results |
This component is currently stable and uses JetBrains Ktor as the underlying HTTP server for hosting Kiit APIs as Web/HTTP APIs.
Feature | Status | Description |
**Open-API** | Upcoming | Auto-generate Open-APIs docs from Actions |
**Postman** | Upcoming | Auto-generate Postman scripts from Actions |
**Serialization** | Upcoming | Replace internal serializer with Jackson, Moshi, or kotlinx.serialization |
**Streaming** | Upcoming | Support for streaming |
Use the following settings in gradle for installing this component.
buildscript {
// Kotlin version currently being used
ext.kotlin_version = '1.8.22'
// Reference version
ext.kiit_version = '3.1.0'
// ...
}
repositories {
// Currently stored in Git Hub Packages.
maven {
url "https://maven.pkg.github.com/slatekit/kiit"
credentials {
username = System.getenv('GIT_PACKAGES_INSTALL_ACTOR')
password = System.getenv('GIT_PACKAGES_INSTALL_TOKEN')
}
}
// ...
}
dependencies {
// Other dependencies ...
implementation "dev.kiit:kiit-apis:$kiit_version"
}
Jar | slatekit.apis.jar |
Package | slatekit.apis |
Sources | slatekit-apis |
Example | Example_APIs.kt |
Requires | See build.gradle for more info. |
This is a quick and simple example of creating an API using the Kiit Universal API paradigm. This API is then accessible on the CLI and Web
import slatekit.apis.*
import slatekit.common.DateTime
import slatekit.common.Request
@Api(area = "app", name = "movies", desc = "Create and manage movies", auth = AuthModes.NONE)
class MovieApi {
// Using explicit fields
@Action()
fun createSample(title:String, playing:Boolean, cost:Int, released: DateTime):Movie {
return Movie.of(title, playing, cost, released)
}
// Using request object
@Action()
fun createWithRequest(req:Request):Movie {
return Movie.of(req.getString("title"), req.getBool("playing"), req.getInt("cost"), req.getDateTime("released"))
}
}
Name | Description | More |
1. Setup | Description of feature | more |
2. Routes | How routes work actions are called | more |
3. Config | Configuration options including auth, verbs, sources and more | more |
4. Requests | How to handle requests and parameters | more |
5. Responses | How to return responses | more |
6. Errors | Modeling of successes, failures and dealing with status codes | more |
7. Middleware | Incorporate middle ware globally or per API | more |
8. Web | How to make APIs hosted in a Web Server (Ktor) | more |
9. CLI | How to make APIs hosted in a CLI ( Kiit CLI ) | more |
10. Security | Authentication and security | more |
11. Load | Performance and load | more |
12. Files | How to handle requests from Files | more |
13. REST | How to set up partial REST compatible actions | more |
APIs are developed as normal Kotlin methods. The only difference is that they are enriched with annotations and/or configuration during registration, to provided metadata to the API Server indicated how they should be accessed and managed.
This approach is convenient and puts all relevant metadata at the source.
import slatekit.apis.*
import slatekit.common.DateTime
import slatekit.common.Request
@Api(area = "manage", name = "movies", desc = "Create and manage movies", auth = AuthModes.NONE)
class MovieApi {
// Using explicit fields
@Action()
fun createSample(title:String, playing:Boolean, cost:Int, released: DateTime):Movie {
return Movie.of(title, playing, cost, released)
}
// Using request object
@Action()
fun createWithRequest(req:Request):Movie {
return Movie.of(req.getString("title"), req.getBool("playing"), req.getInt("cost"), req.getDateTime("released"))
}
}
This approach reduces the dependency on Kiit, and requires that the metadata be supplied during registration of the API, and all the actions assume that the annotation values are inherited from the parent Api metadata.
import slatekit.apis.*
import slatekit.common.DateTime
import slatekit.common.Request
class MovieApi {
// Using explicit fields
fun createSample(title:String, playing:Boolean, cost:Int, released: DateTime):Movie {
return Movie.of(title, playing, cost, released)
}
// Using request object
fun createWithRequest(req:Request):Movie {
return Movie.of(req.getString("title"), req.getBool("playing"), req.getInt("cost"), req.getDateTime("released"))
}
}
// ... Registration code ( see other sections for more details )
val api = slatekit.apis.core.Api(
instance = MovieApi(ctx),
area = "manage",
name = "movies",
auth = AuthMode.None
)
API routes consist of a 3 part ( {AREA} / {API} / {ACTION} ) routing convention to enfore standards and simplify the discovery of the routes.
Part | Example | Note |
AREA | manage | Represents a logical grouping of 1 or more APIs |
API | movies | Represents an actual API associated with a Kotlin class |
ACTION | createSample | Represents an action/endpoint on an API, and maps to a Kotlin method |
POST localhost:5000/manage/movies/createSample
{
"title": "Dark Knight",
"playing": true,
"cost" : 12.50,
"released": "2018-07-18T00:00:00Z"
}
Because the routes are standardized on a 3 part heirarchical format, this makes discovery of all Areas, APIS, Actions, Inputs(for actions) incredibly straight-forward for both Web and CLI hosted APIs. You can drill down from areas into actions using an approach that resembles navigating a folder and showing the items.
Discover | CLI | Web | Note |
Areas | ? | /help | Lists Areas available. E.g. manage, discover |
APIs in Area | manage? | /manage/help | Lists APIs available in the manage area E.g. movies, concerts |
Actions on API | manage.movies? | /manage/movies/help | Lists Actions available in manage/movies API E.g. create, update, delete, disable |
Inputs on Action | manage.movies.createSample? | /manage/movies/createSample/help | Lists Inputs for manage / movies /createSample Action E.g. title=string, cost=double |
There are several annotation properties available to configure APIs and Actions.
These are all the properties for the @Api annotation to put on a class to indicate it should be accessable as an Api.
Type | Name | Required | Default | Purpose | Example |
@Api | area | Required | n/a | Represents the logical area of the API | manage |
- | name | Required | n/a | Represents the name of this API | movies |
- | desc | Optional | Empty | Description of the API | Manages movies |
- | auth | Optional | None | Specifies how the authentication should work | AuthMode.kt |
- | roles | Optional | Empty | List of roles allowed to access this API | Roles.kt |
- | access | Optional | Public | Desribes visibility of the API | Access.kt |
- | verb | Optional | Auto | Desribes how the Verbs should be handled | Verbs.kt |
- | sources | Optional | All | Indicates where this API can handle requests from | Sources.kt |
These are all the properties for the @Action annotation to be put on methods to indicate it should be available as an action/endpoint
@Action | area | Required | Default | Represents the logical area of the API | Example |
- | name | Optional | method name | Represents the name of this Action | createSample |
- | desc | Optional | Empty | Description of the Action | Create sample movie |
- | roles | Optional | Empty | List of roles allowed to access this API | Roles.kt |
- | access | Optional | Public | Desribes visibility of the API | Access.kt |
- | verb | Optional | Auto | Desribes how the Verbs should be handled | Verbs.kt |
- | sources | Optional | All | Indicates where this API can handle requests from | Sources.kt |
Requests in are abstracted out as Request.kt. They are implementations for a Web Request and CLI request.
val request:Request = CommonRequest(
path = "app.users.activate",
parts = listOf("app", "users", "activate"),
source = Source.CLI,
verb = Verbs.POST,
meta = InputArgs(mapOf("api-key" to "ABC-123")),
data = InputArgs(mapOf("userId" to 5001)),
raw = "the raw HTTP SparkJava send or CLI ShellCommand",
tag = Random.uuid()
)
// NOTES: ResultSupport trait builds results that simulate Http Status codes
// This allows easy construction of results/status from a server layer
// instead of a controller/api layer
// CASE 1: Get path of the request, parts, and associated 3 part routes
println( request.path )
println( request.parts )
println( request.area )
println( request.name )
println( request.action )
// CASE 2: Get the verb (For the CLI - the verb will be "cli")
println( request.verb )
// CASE 3: Get the tag
// The request is immutable and the tag field is populated with
// a random guid, so if an error occurs this tag links the request to the error.
println( request.tag )
// CASE 4: Get metadata named "api-key"
println( request.meta.getString("api-key") )
println( request.meta.getInt("sample-id") )
println( request.meta.getIntOrNull("sample-id") )
println( request.meta.getIntOrElse("sample-id", -1) )
// CASE 5: Get a parameter named "userId" as integer
// IF its not there, this will return a 0 as getInt
// returns a non-nullable value.
// The value is obtained from query string if get, otherwise,
// if the request is a post, the value is
// first checked in the body ( json data ) before checking
// the query params
println( request.data.getInt("userId") )
println( request.data.getIntOrNull("userId") )
println( request.data.getIntOrElse("userId", -1) )
There are 2 ways to returns responses/values from methods. The first is to simply return the exact value. The second is to wrap the value into a Result.
// Case 1: Return explicit value( will return an HTTP 200)
return Movie.of(title, playing, cost, released)
// Case 2. Return value wrapped as Outcome<Movie> = Result<Movie, Err>
val movie = Movie.of(title, playing, cost, released)
return Outcomes.success(movie)
JSON Responses from APIs always the following fields. The value field will represent the payload returned from your method. The success, code, msg, err fields are representing by the Result component for modeling successes/failures.
{
"success": true,
"code": 200001,
"meta": null,
"value": {
"version": "1.0.0",
"date": "2019-08-10"
},
"msg": "Success",
"err": null,
"tag": null
}
You can accurately model successes and failures using Result. Result ( and the Outcome typealias ) help to model errors accurately which can then be easily converted to Http status codes and responses.
@Action()
fun createSampleOutcome(req:Request, title:String, playing:Boolean, cost:Int, released: DateTime):Outcome<Movie> {
val result:Outcome<Movie> = when {
!canCreate(req) -> Outcomes.denied ("Not allowed to create")
title.isNullOrEmpty() -> Outcomes.invalid("Title missing")
!playing -> Outcomes.ignored("Movies must be playing")
cost > 20 -> Outcomes.errored("Prices must be reasonable")
else -> {
// Simple simulation of creation
Outcomes.success( Movie.of(title, playing, cost, released) )
}
}
return result
}
You can host APIs as Web APIs using the default Http Engine which is Ktor.
fun runServer() {
// ====================================
// Setup
// 1. Settings ( defaults: port = 5000, prefix = /api/)
val settings = ServerSettings(docs = true, docKey = "abc123")
// 2. APIs ( defaults applied )
val apis = listOf(
slatekit.apis.core.Api(
klass = SampleApi::class,
singleton = SampleApi(ctx)
)
)
// 3. API host
val apiHost = ApiServer.of( ctx, apis, auth = null)
// 4. Ktor handler: Delegates Ktor requests to
val handler = KtorHandler(ctx, settings, apiHost)
// ====================================
// Jet Brains Ktor setup
val server = embeddedServer(Netty, settings.port) {
routing {
// Root
get("/") { ping(call) }
// Your own custom path
get(settings.prefix + "/ping") { ping(call) }
// Your own multi-path route
get("module1/feature1/action1") {
KtorResponse.json(call, Success("action 1 : " + DateTime.now().toString()).toResponse())
}
// Remaining outes beginning with /api/ to be handled by API Server
handler.register(this)
}
}
// Start Ktor server
server.start(wait = true)
}
You can host Kiit Universal APIs on the CLI using the Kiit CLI
// 1. The API keys( DocApi, SetupApi are authenticated using an sample API key )
val keys = listOf(ApiKey( name ="cli", key = "abc", roles = "dev,qa,ops,admin"))
// 2. Authentication
val auth = Authenticator(keys)
// 3. Load all the Universal APIs
val apis = listOf(
slatekit.apis.core.Api(
klass = SampleApi::class,
singleton = SampleApi(ctx)
)
)
// 4. Makes the APIs accessible on the CLI runner
val cli = CliApi(
ctx = ctx,
auth = auth,
settings = CliSettings(enableLogging = true, enableOutput = true),
apiItems = apis,
metaTransform = {
listOf("api-key" to keys.first().key)
},
serializer = {item, type -> Content.csv(slatekit.meta.Serialization.csv().serialize(item))}
)
// 5. Run interactive mode
return cli.run()
You can then access this API on the CLI by hosting it in the Kiit CLI component. The request would like this:
:> app.movies.createSample -title="Dark Knight" -playing=true -cost=12.50 -released="2018-07-18T00:00:00Z"
You can then call this API from a request saved to a file
// Assume file is ~/dev/slatekit/samples/apis/file-sample.json
{
"path": "/app/movies/createSample",
"source": "file",
"version": "1.0",
"tag" : "",
"timestamp": null,
"meta": { }
"data": {
"title": "Dark Knight",
"playing": true,
"cost" : 12.50,
"released": "2018-07-18T00:00:00Z"
}
}
Authentication and security docs coming soon
Performance and load docs coming soon