Slide 1

Slide 1 text

Building Strongly typed web services with Kotlin Ktor Ktor for backend development Julien Salvi (Come to the kotlin side we have cookies)

Slide 2

Slide 2 text

Julien Salvi Lead Android Engineer @ Aircall Android GDE PAUG, Punk and IPAs! @JulienSalvi

Slide 3

Slide 3 text

Table of Contents Introduction Setup a Ktor project First steps with Ktor A Ktor overview A typed web service! Swagger for a reliable contract with clients A source of truth for your API Ktor plugins, Postgres, Exposed… 01 02 03 04

Slide 4

Slide 4 text

Introduction A Ktor overview 01

Slide 5

Slide 5 text

A ktor overview ● Developed and maintained by JetBrains ● Stable release in 2018 ● Latest release 2.0.1 ● Asynchronous framework, written in Kotlin, to build web services, web apps, web clients and more

Slide 6

Slide 6 text

A ktor overview ● Ktor is lightweight. Use the plugins you want with easy configuration. ● Ktor is extensible. Configurable pipeline where you can extend what your need. ● Ktor is multiplatform. Use it on mobile, backends or web apps. ● Ktor is asynchronous. Thanks to Kotlin coroutines we can build high scalable microservices.

Slide 7

Slide 7 text

Let’s build a tiny typed web service ● Setup default routes ● Use Ktor Plugins that allow us to build a typed web service (StatusPage, Resources, ContentNegotiation…) ● Use Exposed for dealing with a PostgreSQL database, Valiktor for data validation ● Use Swagger as a single source of truth

Slide 8

Slide 8 text

Setup a Ktor project First steps with Ktor 02

Slide 9

Slide 9 text

+ (Live from the studio)

Slide 10

Slide 10 text

Build a typed web service! Ktor plugins, Exposed, Valiktor… 03

Slide 11

Slide 11 text

Postgres HTTPS requests (POST, GET, DELETE…): https://api.swcook.io/recipes https://api.swcook.io/recipes/10 https://api.swcook.io/ingredients { "position": 1, "description": "Cut the mushrooms in thin slices", "prep_time": 5 } Shared code CookApp

Slide 12

Slide 12 text

https://api.swcook.io/recipes/1a03-56qg/steps { "position": 1, "description": "Cut the mushrooms in thin slices", "prep_time": 5 } request Route validation Payload validation Constraint validation Response validation entity mappers entity mappers response

Slide 13

Slide 13 text

Features and libraries Features & libraries Purpose Resources Routing ContentNegotiation Negotiating media types and serialization StatusPage Handle exceptions Valiktor Data validation Koin Dependency injection Exposed ORM framework to deal with the Postgres database Logback Logging Flyway Database versioning Moshi JSON library HikariCP JDBC connection pool

Slide 14

Slide 14 text

Resources ● Create routes that are type-safe ● Relies on kotlinx.serialization ● Support basic types (Int, String, Boolean…) by default ● Typed methods for defining route handlers: get, post, patch, delete… implementation("io.ktor:ktor-server-resources:2.0.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3") Plugin: Resources

Slide 15

Slide 15 text

Routing with Resources object Routes { @Serialization @Resources("/recipes") class Recipes { @Serialization @Resources("/{uid}") data class ByUid( val recipes: Recipes, @Serialization(with = UUIDSerializer::class) val uid: UUID ) } }

Slide 16

Slide 16 text

Routing with Resources object Routes { @Serialization @Location("/recipes/{uid}/ingredients/{ingredientUid}") class RecipeIngredientDetails( @Serialization(with = UUIDSerializer::class) val uid: UUID, @Serialization(with = UUIDSerializer::class) val ingredientUid: UUID ) }

Slide 17

Slide 17 text

Routing with Resources object UUIDSerializer : KSerializer { override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) override fun deserialize(decoder: Decoder): UUID { return UUID.fromString(decoder.decodeString()) } override fun serialize(encoder: Encoder, value: UUID) { encoder.encodeString(value.toString()) } }

Slide 18

Slide 18 text

Routing with Resources fun Application.module(testing: Boolean = false) { install(Resources) routing { recipes() } } // RecipeController.kt fun Route.recipes() { get { } post { } patch { route -> } delete { route -> } }

Slide 19

Slide 19 text

Routing with Resources // GET request with parameters get { val page = call.parameters["page"].toInt() val size = call.parameters["size"].toInt() ... call.respond(HttpStatusCode.OK, response) } // POST request with body post { val request = call.receive() } // POST request with multipart data (upload files for example) post { route -> val uid = route.uid val multipart = call.receiveMultipart() }

Slide 20

Slide 20 text

Moshi ● JSON library for made by Square ● Deserialize data received from requests and serialize responses ● Lightweight, no HTML escaping, custom type adapters ● Used by Swagger gradle plugin 👀 implementation("com.squareup.moshi:moshi:1.13.0") square/moshi

Slide 21

Slide 21 text

Moshi @JsonClass(generateAdapter = true) data class GetIngredientResponse( @Json(name = "ingredient") @field:Json(name = "ingredient") var ingredient: Ingredient )

Slide 22

Slide 22 text

Valiktor ● Data validation for requests ● Type-safe DSL to validate objects ● Supports suspending functions natively ● Multiple extensions (Spring, Joda… ) implementation("org.valiktor:valiktor-core:0.12.0") Github: Valiktor

Slide 23

Slide 23 text

Valiktor """ { "position": 1, "description": "Cut the mushrooms in thin slices", "prep_time": 5 } """ @JsonClass(generateAdapter = true) data class PostStepRequest( @Json(name = "position") @field:Json(name = "position") var position: Long, @Json(name = "description") @field:Json(name = "description") var description: String, @Json(name = "prep_time") @field:Json(name = "prep_time") var prepTime: Long )

Slide 24

Slide 24 text

Valiktor @Throws(ConstraintViolationException::class) fun PostStepRequest.validate() { validate(this) { validate(PostStepRequest::position).isNotNull() validate(PostStepRequest::position).isGreaterThanOrEqualTo(0) validate(PostStepRequest::description).isNotNull() validate(PostStepRequest::description).isNotBlank() validate(PostStepRequest::prepTime).isNotNull() validate(PostStepRequest::prepTime).isGreaterThanOrEqualTo(0) } }

Slide 25

Slide 25 text

Valiktor post { withContext(Dispatchers.IO) val request = call.receive() request.validate() val created = recipeService.add(request.recipe.toEntity()) if (created != null) { val response = PostRecipeResponse(recipe = created.renderer()) call.respond(HttpStatusCode.Created, response) } else { call.respond(HttpStatusCode.Conflict) } } }

Slide 26

Slide 26 text

Error Handling: Status Page ● Properly handle errors and exceptions in the application ● 3 configurations: exceptions, status and statusFile ● Simple configuration Plugin: StatusPage implementation("io.ktor:ktor-server-status-pages:2.0.1")

Slide 27

Slide 27 text

StatusPage fun Application.module(testing: Boolean = false) { install(StatusPage) { exceptions() } }

Slide 28

Slide 28 text

StatusPage fun StatusPages.Configuration.exceptions() { exception { call, exception -> // Basic ConstraintViolationException handler val violations = exception.constraintViolations.map { violation -> "${violation.property}:${violation.constraint.name}" } call.respondText(status = HttpStatusCode.UnprocessableEntity) { violations.toString() } } exception { call, exception -> call.application.log.error("Unhandled exception", exception) call.respond(HttpStatusCode.InternalServerError) } }

Slide 29

Slide 29 text

Setup Postgres Database From Docker with love 🐳

Slide 30

Slide 30 text

PostgreSQL database with Docker Compose ● Persistent data with Postgres database ● Run PostgreSQL database and pgadmin on a Docker container thanks to Docker Compose ● Compose is a tool to run multi-container Docker apps ● Use a YAML file to configure services

Slide 31

Slide 31 text

Exposed ● ORM framework for Kotlin by JetBrains ● Supports PostgreSQL, MySQL, H2, MariaDB, SQLite… + DataSource JDBC connection with HikariCP ● DSL or DAO API for querying the database ● Supports basic and advanced CRUD operations Github: Exposed

Slide 32

Slide 32 text

Gradle Dependencies implementation("org.jetbrains.exposed:exposed-core:0.38.2") implementation("org.jetbrains.exposed:exposed-dao:0.38.2") implementation("org.jetbrains.exposed:exposed-jdbc:0.38.2") implementation("org.jetbrains.exposed:exposed-java-time:0.38.2")

Slide 33

Slide 33 text

Setup Tables and DAO object RecipeTable : ExtendedUUIDTable(name = "recipe") { val title: Column = text(name = "title") val description: Column = text(name = "description") val cookingTime: Column = integer(name = "cooking_time").default(0) } class Recipe(id: EntityID) : ExtendedUUIDEntity(id, RecipeTable) { companion object : ExtendedUUIDEntityClass(RecipeTable) var title by RecipeTable.title var description by RecipeTable.description var cookingTime by RecipeTable.cookingTime val steps by Step referrersOn StepTable.recipe var ingredients by Ingredient via RecipeIngredientTable }

Slide 34

Slide 34 text

CRUD Operations // CREATE val recipe = Recipe.new { title = recipe.title description = recipe.description cookingTime = recipe.cookingTime } // READ val recipe = Recipe.findById(id) // Ingredient.find { IngredientTable.id eq id }.first() // UPDATE val recipe = Recipe.findById(id) recipe?.title = title // DELETE val recipe = Recipe.findById(id) recipe?.delete()

Slide 35

Slide 35 text

Transactions // Basic transaction val ingredient = transaction { createIngredient(entity) } // Coroutine support with suspend functions val ingredient = newSuspendedTransaction { Ingredient.findById(id)?.toEntity() } // Deferred transaction suspendedTransactionAsync(Dispatchers.IO) { Recipe.findById(recipeId)?.ingredients?.map(Ingredient::toEntity) }

Slide 36

Slide 36 text

Referencing // Many-to-one object StepTable : ExtendedUUIDTable(name = "step") { ... val recipe = reference("recipe", RecipeTable) val recipe = reference("video", VideoTable).nullable() } class Step(id: EntityID) : ExtendedUUIDEntity(id, StepTable) { ... var recipe by Recipe referencedOn StepTable.recipe var video by Video optionalReferencedOn StepTable.video } class Recipe(id: EntityID) : ExtendedUUIDEntity(id, RecipeTable) { ... val steps by Step referrersOn StepTable.recipe }

Slide 37

Slide 37 text

Referencing // Many-to-many object RecipeIngredientTable : Table() { val recipe = reference(RecipeTable.tableName, RecipeTable) val ingredient = reference(IngredientTable.tableName, IngredientTable) override val primaryKey = PrimaryKey(recipe, ingredient) } class Recipe(id: EntityID) : ExtendedUUIDEntity(id, RecipeTable) { ... var ingredients by Ingredient via RecipeIngredientTable } class Ingredient(id: EntityID) : ExtendedUUIDEntity(id, IngredientTable) { ... var recipes by Recipe via RecipeIngredientTable }

Slide 38

Slide 38 text

Swagger for a reliable contract with clients A source of truth for your API 04

Slide 39

Slide 39 text

Swagger Gradle Codegen ● Gradle plugin to generate network code from Swagger file by Nicola Corti ● Swagger relies on OpenAPI for RESTful web services, supports YAML or JSON formats ● Generates Retrofit interfaces, uses Moshi for serialization and supports Kotlin coroutines ● Having a single source of truth Yelp/swagger-gradle-codegen

Slide 40

Slide 40 text

Swagger module swagger-api/ ├── .build/ │ └── com/example/swcook/front │ ├── apis/ │ ├── models/ │ └── tools ├── api-client.yml └── build.gradle.kts

Slide 41

Slide 41 text

Swagger module swagger-api/ ├── .build/ │ └── com/example/swcook/front │ ├── apis/ │ ├── models/ │ └── tools ├── api-client.yml └── build.gradle.kts

Slide 42

Slide 42 text

Swagger: gradle build plugins { id("com.yelp.codegen.plugin") version "1.4.1" } generateSwagger { platform = "kotlin-coroutines" packageName = "com.example.swcook.front" inputFile = file("./api-client.yml") outputDir = file("./build/") }

Slide 43

Slide 43 text

Swagger: define paths paths: /recipes/{recipe_id}: get: tags: - recipes summary: Get recipe details operationId: "getRecipe" parameters: - name: recipe_id in: path required: true type: string format: uuid responses: '200': description: All recipes details schema: $ref: '#/definitions/Recipe'

Slide 44

Slide 44 text

Swagger: define models definitions: Recipe: type: object required: - id - title - cooking_time properties: id: type: string format: uuid title: type: string description: type: string cooking_time: type: integer format: int32 date: type: string format: date-time steps: type: array items: $ref: '#/definitions/Step' ingredients: type: array items: $ref: '#/definitions/Ingredient'

Slide 45

Slide 45 text

Swagger: retrofit & models @JvmSuppressWildcards interface RecipesApi { /** * Get recipe details * @param recipeId (required) */ @Headers("Content-Type: application/json") @GET("/recipes/{recipe_id}") suspend fun getRecipe( @retrofit2.http.Path("recipe_id") recipeId: UUID ): Recipe }

Slide 46

Slide 46 text

Swagger: retrofit & models @JsonClass(generateAdapter = true) data class Recipe( @Json(name = "id") @field:Json(name = "id") var id: UUID, @Json(name = "title") @field:Json(name = "title") var title: String, @Json(name = "description") @field:Json(name = "description") var description: String, @Json(name = "cooking_time") @field:Json(name = "cooking_time") var cookingTime: Int, @Json(name = "date") @field:Json(name = "date") var date: ZonedDateTime? = null, @Json(name = "steps") @field:Json(name = "steps") var steps: List? = null, @Json(name = "ing") @field:Json(name = "ing") var ing: List? = null, )

Slide 47

Slide 47 text

Swagger Gradle Codegen ● Front models for your backend and remote models for your Android app will be the same ● No more object type surprise! ● Strong contract between clients and APIs ● Improvements: kotlinx.serialization support, date type config… Yelp/swagger-gradle-codegen

Slide 48

Slide 48 text

Live Demo (star) Watch the Ktor web service in action !

Slide 49

Slide 49 text

CREDITS: This presentation template was created by Slidesgo, including icons by Flaticon, infographics & images by Freepik Thanks! Do you have any questions? Have fun with Ktor Julien Salvi