Ktor to your heart’s content: easy mobile backends in Kotlin

Ktor to your heart’s content: easy mobile backends in Kotlin

Most Android developers are familiar with Kotlin and use it every day with great success (and joy!). But the bloodline of the language extends well beyond Android! From cross-platform development, to the desktop, to the web, to backends, Kotlin’s pedigree as a versatile language puts all us developers in a great position to leverage existing knowledge for a variety of tasks outside a mobile engineer’s comfort zone. In this session we’ll use Kotlin, Ktor and coroutines to write a backend for our splendid fictional mobile app product: PushBeat.

After learning the basics of a RESTful API, we’ll see what Ktor takes care of for us, what features may be helpful for our mobile projects, and how little we actually need to learn to be a productive backend engineer in no time!

---

The original deck (Keynote 10.0) is available here: https://www.dropbox.com/s/rhohmdi81sxah34/Ktor%20to%20your%20heart%27s%20content.key.zip?dl=0

The recording is here: https://www.youtube.com/watch?v=p8RA-3t0jGA

The sample project is here: https://github.com/rock3r/pushbeat

4580c218737149bf44d012a110612010?s=128

Sebastiano Poggi

June 30, 2020
Tweet

Transcript

  1. Sebastiano Poggi @seebrock3r Easy mobile backends in Kotlin Ktor to

    your heart’s content
  2. None
  3. None
  4. None
  5. REST GraphQL (g)RPC

  6. REST GraphQL (g)RPC Firebase

  7. REST GraphQL (g)RPC • Oldest and simplest of them all

    • More of a style than a protocol • Not a standard, uses standards in the stack • Basic concepts: methods and resources • Most widely used of the three • Available pretty much everywhere
  8. REST GraphQL (g)RPC • Graph-based API language created by Facebook

    • Not a standard, but has OSS specification • Served over HTTP • Single endpoint, using a query language
  9. REST GraphQL (g)RPC • Remote Procedure Call protocol, by Google

    • Not a standard, but has OSS specification • Served over HTTP/2 • Based on protocol buffers (protobuf) • Generates type-safe client and server bindings
  10. REST GraphQL (g)RPC What’s the best for you?

  11. …it depends! REST GraphQL (g)RPC What’s the best for you?

  12. REST • Fully supported by Ktor • Pragmatic “RESTful enough”

  13. Ktor

  14. Ktor • Kotlin-based framework for async clients and servers •

    Familiar language for Android devs • Easy async support with coroutines • Unopinionated • Pick the pieces you need and like • Testable Docs at ktor.io
  15. Ktor • Covers both servers and clients • Support for

    Kotlin/Multiplatform (client) • Support for REST and WebSockets • Support for auth: basic, token-based (OAuth) Docs at ktor.io
  16. Ktor • Our example RESTful server • REST APIs serving

    JSON • HTML endpoints • Authentication • Running locally and deploying to AppEngine Docs at ktor.io
  17. PushBeat PushBeat You seem ver e se What about taking

    a short break • Fictional IoT service & Android app • Track your heartbeat when you push code • If it’s too high, it suggests you take a break Code at github.com/rock3r/pushbeat
  18. $ curl https:"#sdk.cloud.google.com | bash 1 2 3 Set up

    AppEngine SDK 1. Install Google Cloud SDK 2. Create GCP project and enable billing
  19. $ cd ~/path/to/project "$ gcloud init 5. Select your project

    $ gcloud auth login $ gcloud components install app-engine-java 4. Login to GCP Set up AppEngine SDK 3. Install AppEngine Java component
  20. build.gradle.kts import com.google.cloud.tools.gradle.appengine.standard.AppEngineStandardExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") kotlin("plugin.serialization") version

    "1.3.72" war } buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath("com.google.cloud.tools:appengine-gradle-plugin:2.2.0") } } "# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure<AppEngineStandardExtension> { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } Set up Ktor and AppEngine
  21. import com.google.cloud.tools.gradle.appengine.standard.AppEngineStandardExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") kotlin("plugin.serialization") version "1.3.72"

    war } buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath("com.google.cloud.tools:appengine-gradle-plugin:2.2.0") } } "# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure<AppEngineStandardExtension> { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] Set up Ktor and AppEngine build.gradle.kts
  22. import com.google.cloud.tools.gradle.appengine.standard.AppEngineStandardExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") kotlin("plugin.serialization") version "1.3.72"

    war } buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath("com.google.cloud.tools:appengine-gradle-plugin:2.2.0") } } "# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure<AppEngineStandardExtension> { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] Set up Ktor and AppEngine build.gradle.kts
  23. import com.google.cloud.tools.gradle.appengine.standard.AppEngineStandardExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") kotlin("plugin.serialization") version "1.3.72"

    war } buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath("com.google.cloud.tools:appengine-gradle-plugin:2.2.0") } } "# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure<AppEngineStandardExtension> { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] Set up Ktor and AppEngine build.gradle.kts
  24. appengine { … } Groovy DSL: import com.google.cloud.tools.gradle.appengine.standard.AppEngineStandardExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

    plugins { kotlin("jvm") kotlin("plugin.serialization") version "1.3.72" war } buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath("com.google.cloud.tools:appengine-gradle-plugin:2.2.0") } } "# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure<AppEngineStandardExtension> { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] Set up Ktor and AppEngine build.gradle.kts
  25. import com.google.cloud.tools.gradle.appengine.standard.AppEngineStandardExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") kotlin("plugin.serialization") version "1.3.72"

    war } buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath("com.google.cloud.tools:appengine-gradle-plugin:2.2.0") } } "# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure<AppEngineStandardExtension> { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] Set up Ktor and AppEngine cloud.google.com/sdk/install build.gradle.kts
  26. buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath("com.google.cloud.tools:appengine-gradle-plugin:2.2.0")

    } } "# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure<AppEngineStandardExtension> { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] implementation(kotlin("stdlib-jdk8")) implementation("ch.qos.logback:logback-classic:1.2.3") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") implementation("io.ktor:ktor-server-servlet:$ktorVersion") implementation("io.ktor:ktor-html-builder:$ktorVersion") implementation("io.ktor:ktor-auth:$ktorVersion") Set up Ktor and AppEngine build.gradle.kts
  27. "# Note: the AppEngine plugin currently does something funky when

    you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure<AppEngineStandardExtension> { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] implementation(kotlin("stdlib-jdk8")) implementation("ch.qos.logback:logback-classic:1.2.3") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") implementation("io.ktor:ktor-server-servlet:$ktorVersion") implementation("io.ktor:ktor-html-builder:$ktorVersion") implementation("io.ktor:ktor-auth:$ktorVersion") implementation("io.ktor:ktor-locations:$ktorVersion") implementation("io.ktor:ktor-serialization:$ktorVersion") implementation("io.ktor:ktor-server-host-common:$ktorVersion") providedCompile("com.google.appengine:appengine:1.9.60") testImplementation("io.ktor:ktor-server-tests:$ktorVersion") } Set up Ktor and AppEngine build.gradle.kts
  28. "# Note: the AppEngine plugin currently does something funky when

    you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure<AppEngineStandardExtension> { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] implementation(kotlin("stdlib-jdk8")) implementation("ch.qos.logback:logback-classic:1.2.3") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") implementation("io.ktor:ktor-server-servlet:$ktorVersion") implementation("io.ktor:ktor-html-builder:$ktorVersion") implementation("io.ktor:ktor-auth:$ktorVersion") implementation("io.ktor:ktor-locations:$ktorVersion") implementation("io.ktor:ktor-serialization:$ktorVersion") implementation("io.ktor:ktor-server-host-common:$ktorVersion") providedCompile("com.google.appengine:appengine:1.9.60") testImplementation("io.ktor:ktor-server-tests:$ktorVersion") } Set up Ktor and AppEngine Features build.gradle.kts
  29. "# Note: the AppEngine plugin currently does something funky when

    you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure<AppEngineStandardExtension> { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] implementation(kotlin("stdlib-jdk8")) implementation("ch.qos.logback:logback-classic:1.2.3") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") implementation("io.ktor:ktor-server-servlet:$ktorVersion") implementation("io.ktor:ktor-html-builder:$ktorVersion") implementation("io.ktor:ktor-auth:$ktorVersion") implementation("io.ktor:ktor-locations:$ktorVersion") implementation("io.ktor:ktor-serialization:$ktorVersion") implementation("io.ktor:ktor-server-host-common:$ktorVersion") providedCompile("com.google.appengine:appengine:1.9.60") testImplementation("io.ktor:ktor-server-tests:$ktorVersion") } Set up Ktor and AppEngine build.gradle.kts
  30. "# Note: the AppEngine plugin currently does something funky when

    you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure<AppEngineStandardExtension> { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] implementation(kotlin("stdlib-jdk8")) implementation("ch.qos.logback:logback-classic:1.2.3") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") implementation("io.ktor:ktor-server-servlet:$ktorVersion") implementation("io.ktor:ktor-html-builder:$ktorVersion") implementation("io.ktor:ktor-auth:$ktorVersion") implementation("io.ktor:ktor-locations:$ktorVersion") implementation("io.ktor:ktor-serialization:$ktorVersion") implementation("io.ktor:ktor-server-host-common:$ktorVersion") providedCompile("com.google.appengine:appengine:1.9.60") testImplementation("io.ktor:ktor-server-tests:$ktorVersion") } Set up Ktor and AppEngine build.gradle.kts
  31. build.gradle.kts "# Note: the AppEngine plugin currently does something funky

    when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure<AppEngineStandardExtension> { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] implementation(kotlin("stdlib-jdk8")) implementation("ch.qos.logback:logback-classic:1.2.3") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") implementation("io.ktor:ktor-server-servlet:$ktorVersion") implementation("io.ktor:ktor-html-builder:$ktorVersion") implementation("io.ktor:ktor-auth:$ktorVersion") implementation("io.ktor:ktor-locations:$ktorVersion") implementation("io.ktor:ktor-serialization:$ktorVersion") implementation("io.ktor:ktor-server-host-common:$ktorVersion") providedCompile("com.google.appengine:appengine:1.9.60") testImplementation("io.ktor:ktor-server-tests:$ktorVersion") } Set up Ktor and AppEngine
  32. Ktor and AppEngine configuration • src/main/resources/application.conf • Ktor main configuration

  33. • src/main/resources/application.conf • Ktor main configuration • src/main/resources/logback.xml • Logging

    configuration • Two appenders: STDOUT and CLOUD Ktor and AppEngine configuration
  34. • src/main/webapp/WEB-INF/appengine-web.xml • AppEngine main configuration Ktor and AppEngine configuration

  35. • src/main/webapp/WEB-INF/appengine-web.xml • AppEngine main configuration • src/main/webapp/WEB-INF/logging.properties • Cloud

    logging configuration Ktor and AppEngine configuration
  36. • src/main/webapp/WEB-INF/appengine-web.xml • AppEngine main configuration • src/main/webapp/WEB-INF/logging.properties • Cloud

    logging configuration • src/main/webapp/WEB-INF/web.xml • Servlet configuration Ktor and AppEngine configuration
  37. package dev.sebastiano.pushbeat.api @Suppress(“unused") "# Referenced in application.conf fun Application.module() {

    "# ""& } • In Application.kt ktor { deployment { port = 8080 port = ${?PORT} } application { modules = [ dev.sebastiano.pushbeat.api.ApplicationKt.module ] } } • What to start is defined in application.conf Start your server up
  38. package dev.sebastiano.pushbeat.api @Suppress(“unused") "# Referenced in application.conf fun Application.module() {

    "# ""& } • In Application.kt ktor { deployment { port = 8080 port = ${?PORT} } application { modules = [ dev.sebastiano.pushbeat.api.ApplicationKt.module ] } } • What to start is defined in application.conf Start your server up
  39. package dev.sebastiano.pushbeat.api @Suppress(“unused") "# Referenced in application.conf fun Application.module() {

    "# ""& } • In Application.kt ktor { deployment { port = 8080 port = ${?PORT} } application { modules = [ dev.sebastiano.pushbeat.api.ApplicationKt.module ] } } • What to start is defined in application.conf Start your server up
  40. Features • Modular design • Features • Pluggable pipeline •

    Everything is a feature • Routing, auth, content negotiation, etc. • Install only the features you need
  41. @Suppress("unused") "# Referenced in application.conf @JvmOverloads fun Application.module(testing: Boolean =

    false) { setupAuthentication() install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) { gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO Features install(Authentication) { basic(AUTHENTICATOR_NAME) { realm = "PushBeat" validate { if (it.name "' "push" "$ it.password "' “beat”) { UserIdPrincipal(it.name) } else null } } }
  42. @Suppress("unused") "# Referenced in application.conf @JvmOverloads fun Application.module(testing: Boolean =

    false) { setupAuthentication() install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) { gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO Features
  43. @Suppress("unused") "# Referenced in application.conf @JvmOverloads fun Application.module(testing: Boolean =

    false) { setupAuthentication() install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) { gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO Features
  44. @Suppress("unused") "# Referenced in application.conf @JvmOverloads fun Application.module(testing: Boolean =

    false) { setupAuthentication() install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) { gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO Features
  45. install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) {

    gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO format { call #$ when (val status = call.response.status() "( "Unhandled") { HttpStatusCode.Found ") "$status: ${call.request.httpMethod.value} - " + "${call.request.uri} [accept: '${call.request.accept()}'] ") " + "${call.response.headers[HttpHeaders.Location]}" else ") "$status: ${call.request.httpMethod.value} - ${call.request.uri}" + "[accept: '${call.request.accept()}']" } } } setupStatusPages(json, logger) setupRouting(json, BeatSourcesRegistry) } Features
  46. install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) {

    gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO format { call #$ when (val status = call.response.status() "( "Unhandled") { HttpStatusCode.Found ") "$status: ${call.request.httpMethod.value} - " + "${call.request.uri} [accept: '${call.request.accept()}'] ") " + "${call.response.headers[HttpHeaders.Location]}" else ") "$status: ${call.request.httpMethod.value} - ${call.request.uri}" + "[accept: '${call.request.accept()}']" } } } setupStatusPages(json, logger) setupRouting(json, BeatSourcesRegistry) } Features
  47. install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) {

    gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO format { call #$ when (val status = call.response.status() "( "Unhandled") { HttpStatusCode.Found ") "$status: ${call.request.httpMethod.value} - " + "${call.request.uri} [accept: '${call.request.accept()}'] ") " + "${call.response.headers[HttpHeaders.Location]}" else ") "$status: ${call.request.httpMethod.value} - ${call.request.uri}" + "[accept: '${call.request.accept()}']" } } } setupStatusPages(json, logger) setupRouting(json, BeatSourcesRegistry) } Features internal fun Application.setupStatusPages(json: Json, logger: Logger) { install(StatusPages) { status(HttpStatusCode.NotFound) { statusCode #$ val message = generate404ErrorMessage(call.request.uri) call.respondError(json, message, statusCode) } exception<AuthenticationException> { call.respondError(json, "Unauthorized", HttpStatusCode.Unauthorized) } exception<AuthorizationException> { call.respondError(json, "Forbidden", HttpStatusCode.Unauthorized) } exception<Throwable> { cause #$ logger.info("Unhandled exception: ${cause.message}", cause) val errorMessage = "Error while processing the request. ${cause.message}" call.respondError(json, errorMessage, HttpStatusCode.InternalServerError) } } }
  48. install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) {

    gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO format { call #$ when (val status = call.response.status() "( "Unhandled") { HttpStatusCode.Found ") "$status: ${call.request.httpMethod.value} - " + "${call.request.uri} [accept: '${call.request.accept()}'] ") " + "${call.response.headers[HttpHeaders.Location]}" else ") "$status: ${call.request.httpMethod.value} - ${call.request.uri}" + "[accept: '${call.request.accept()}']" } } } setupStatusPages(json, logger) setupRouting(json, BeatSourcesRegistry) } Features
  49. routing { get("/") { } } Routing

  50. routing { get("/") { } post("/") { } put("/") {

    } delete("/") { } } Routing
  51. routing { get("/") { } post("/") { } put("/") {

    } delete("/") { } static("static") { defaultResource("404.html") } } Routing
  52. routing { get("/") { } post("/") { } put("/") {

    } delete("/") { } static("static") { defaultResource("404.html") } } Routing trace { logger.trace(it.buildText()) }
  53. routing { get("/") { } } Responding to a request

  54. routing { get("/") { } } Responding to a request

    :appengineRun :appengineDeploy call.respondHtml { head { title { +"PushBeat" } } body { h1 { +"PushBeat says hi!" } } }
  55. routing { get("/") { } } call.respondText( contentType = ContentType.Application.Json,

    text = """{ "status": "OK" }""", status = HttpStatusCode.OK ) Responding to a request
  56. routing { } Responding to a request get("/{id}") { val

    id = call.parameters["id"] val name = call.request.queryParameters["name"] call.respondText( contentType ="""{ "id": "$id", "name": "$name" }""", contentType = ContentType.Application.Json )
  57. internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get("/")

    { call.respondHtml { renderRegisteredBeats(registry) } } get("/beats") { call.handleGetBeatSources(json, registry) } get("/beat/{id}") { call.handleGetBeat(json, registry) } put("/beat/{id}") { call.handlePutBeat(json, registry) } delete("/beat/{id}") { call.handleDeleteBeat(json, registry) } get("/beat/{id}/refresh") { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Routing in PushBeat
  58. internal fun HTML.renderRegisteredBeats(registry: BeatSourcesRegistry) { head { title { +"PushBeat"

    } styleLink("/static/css/pushbeat.css") script(src = "/static/js/pushbeat.js", type = LinkType.textJavaScript) {} } body { h1 { +"PushBeat" } h2 { +"Registered beats" } ul { val sources = registry.sources() if (sources.isEmpty()) { li { i { +"None" } } return@ul } for (source in sources) { li { p { b { +"${source.name} " span(classes = "secondary") { +"(id: " code { +source.id } +")" } } br Routing in PushBeat
  59. internal fun HTML.renderRegisteredBeats(registry: BeatSourcesRegistry) { head { title { +"PushBeat"

    } styleLink("/static/css/pushbeat.css") script(src = "/static/js/pushbeat.js", type = LinkType.textJavaScript) {} } body { h1 { +"PushBeat" } h2 { +"Registered beats" } ul { val sources = registry.sources() if (sources.isEmpty()) { li { i { +"None" } } return@ul } for (source in sources) { li { p { b { +"${source.name} " span(classes = "secondary") { +"(id: " code { +source.id } +")" } } br +"Last BPM: ${source.beats.valueOrNull "( "N/A"}" span(classes = "refresh") { onClick = "javascript:refreshBeatValue('${source.id}'); location.reload();" +"refresh" } } } Routing in PushBeat
  60. internal fun HTML.renderRegisteredBeats(registry: BeatSourcesRegistry) { head { title { +"PushBeat"

    } styleLink("/static/css/pushbeat.css") script(src = "/static/js/pushbeat.js", type = LinkType.textJavaScript) {} } body { h1 { +"PushBeat" } h2 { +"Registered beats" } ul { val sources = registry.sources() if (sources.isEmpty()) { li { i { +"None" } } return@ul } for (source in sources) { li { p { b { +"${source.name} " span(classes = "secondary") { +"(id: " code { +source.id } +")" } } br +"Last BPM: ${source.beats.valueOrNull "( "N/A"}" span(classes = "refresh") { onClick = "javascript:refreshBeatValue('${source.id}'); location.reload();" +"refresh" } } } } } } } Routing in PushBeat
  61. title { +"PushBeat" } styleLink("/static/css/pushbeat.css") script(src = "/static/js/pushbeat.js", type =

    LinkType.textJavaScript) {} } body { h1 { +"PushBeat" } h2 { +"Registered beats" } ul { val sources = registry.sources() if (sources.isEmpty()) { li { i { +"None" } } return@ul } for (source in sources) { li { p { b { +"${source.name} " span(classes = "secondary") { +"(id: " code { +source.id } +")" } } br +"Last BPM: ${source.beats.valueOrNull "( "N/A"}" span(classes = "refresh") { onClick = "javascript:refreshBeatValue('${source.id}'); location.reload();" +"refresh" } } } } } } } Routing in PushBeat
  62. internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get("/")

    { call.respondHtml { renderRegisteredBeats(registry) } } get("/beats") { call.handleGetBeatSources(json, registry) } get("/beat/{id}") { call.handleGetBeat(json, registry) } put("/beat/{id}") { call.handlePutBeat(json, registry) } delete("/beat/{id}") { call.handleDeleteBeat(json, registry) } get("/beat/{id}/refresh") { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") defaultResource("404.html") } } } private fun Route.resourceFolder(name: String) { static(name) { resources(name) } } Routing in PushBeat
  63. internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get("/")

    { call.respondHtml { renderRegisteredBeats(registry) } } get("/beats") { call.handleGetBeatSources(json, registry) } get("/beat/{id}") { call.handleGetBeat(json, registry) } put("/beat/{id}") { call.handlePutBeat(json, registry) } delete("/beat/{id}") { call.handleDeleteBeat(json, registry) } get("/beat/{id}/refresh") { call.handleGetRefreshBeat(json, registry) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations
  64. internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get<RootLocation>

    { call.respondHtml { renderRegisteredBeats(registry) } } get<BeatsLocation> { call.handleGetBeatSources(json, registry) } get<BeatLocation> { call.handleGetBeat(json, registry, it.id) } put<BeatLocation> { call.handlePutBeat(json, registry, it.id) } delete<BeatLocation> { call.handleDeleteBeat(json, registry, it.id) } get<BeatLocation.Refresh> { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations
  65. internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get<RootLocation>

    { call.respondHtml { renderRegisteredBeats(registry) } } get<BeatsLocation> { call.handleGetBeatSources(json, registry) } get<BeatLocation> { call.handleGetBeat(json, registry, it.id) } put<BeatLocation> { call.handlePutBeat(json, registry, it.id) } delete<BeatLocation> { call.handleDeleteBeat(json, registry, it.id) } get<BeatLocation.Refresh> { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations
  66. @Location("/") class RootLocation @Location("/beats") class BeatsLocation @Location("/beat/{id}") class BeatLocation(val id:

    String) { @Location("/refresh") class Refresh(val beatLocation: BeatLocation) } SourcesRegistry) { ation.id) Improving routing: locations
  67. @Location("/beat/{id}") class BeatLocation(val id: String) { @Location("/refresh") class Refresh( val

    beatLocation: BeatLocation ) } internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get<RootLocation> { call.respondHtml { renderRegisteredBeats(registry) } } get<BeatsLocation> { call.handleGetBeatSources(json, registry) } get<BeatLocation> { call.handleGetBeat(json, registry, it.id) } put<BeatLocation> { call.handlePutBeat(json, registry, it.id) } delete<BeatLocation> { call.handleDeleteBeat(json, registry, it.id) } get<BeatLocation.Refresh> { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations
  68. @Location("/beat/{id}") class BeatLocation(val id: String) { @Location("/refresh") class Refresh( val

    beatLocation: BeatLocation ) } internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get<RootLocation> { call.respondHtml { renderRegisteredBeats(registry) } } get<BeatsLocation> { call.handleGetBeatSources(json, registry) } get<BeatLocation> { call.handleGetBeat(json, registry, it.id) } put<BeatLocation> { call.handlePutBeat(json, registry, it.id) } delete<BeatLocation> { call.handleDeleteBeat(json, registry, it.id) } get<BeatLocation.Refresh> { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations
  69. @Location("/beat/{id}") class BeatLocation(val id: String) { @Location("/refresh") class Refresh( val

    beatLocation: BeatLocation ) } internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get<RootLocation> { call.respondHtml { renderRegisteredBeats(registry) } } get<BeatsLocation> { call.handleGetBeatSources(json, registry) } get<BeatLocation> { call.handleGetBeat(json, registry, it.id) } put<BeatLocation> { call.handlePutBeat(json, registry, it.id) } delete<BeatLocation> { call.handleDeleteBeat(json, registry, it.id) } get<BeatLocation.Refresh> { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations
  70. internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { authenticate(AUTHENTICATOR_NAME)

    { } static("static") { Basic authentication get<RootLocation> { call.respondHtml { renderRegisteredBeats(registry) } } get<BeatsLocation> { call.handleGetBeatSources(json, registry) } get<BeatLocation> { call.handleGetBeat(json, registry, it.id) } put<BeatLocation> { call.handlePutBeat(json, registry, it.id) } delete<BeatLocation> { call.handleDeleteBeat(json, registry, it.id) } get<BeatLocation.Refresh> { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) }
  71. al fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { uting { authenticate(AUTHENTICATOR_NAME)

    { } static("static") { Basic authentication get<RootLocation> { call.respondHtml { renderRegisteredBeats(registry) } } get<BeatsLocation> { call.handleGetBeatSources(json, registry) } get<BeatLocation> { call.handleGetBeat(json, registry, it.id) } put<BeatLocation> { call.handlePutBeat(json, registry, it.id) } delete<BeatLocation> { call.handleDeleteBeat(json, registry, it.id) } get<BeatLocation.Refresh> { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } ""* Get registered beat providers (HTML) GET http:"#localhost:8080/ Accept: 'text/html'
  72. al fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { uting { authenticate(AUTHENTICATOR_NAME)

    { } static("static") { Basic authentication get<RootLocation> { call.respondHtml { renderRegisteredBeats(registry) } } get<BeatsLocation> { call.handleGetBeatSources(json, registry) } get<BeatLocation> { call.handleGetBeat(json, registry, it.id) } put<BeatLocation> { call.handlePutBeat(json, registry, it.id) } delete<BeatLocation> { call.handleDeleteBeat(json, registry, it.id) } get<BeatLocation.Refresh> { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } ""* Get registered beat providers (HTML) GET http:"#localhost:8080/ Accept: 'text/html' Authorization: Basic push beat
  73. Image credits Some images designed by Freepik / rawpixel.com, macrovector;

    octicons Question time!
  74. Thanks!