Navigation
Search
|
Intro to Ktor: The HTTP server for Kotlin
Wednesday January 8, 2025. 10:00 AM , from InfoWorld
Setting up Ktor Recently, I wrote an introduction to Express.js in a two-part series that starts from the basics and advances to using a datastore with templates and HTMX for dynamic UI interactions. We’ll do the same kind of thing here, with Ktor. One difference is that we’ll use Kotlin’s HTML DSL, or domain-specific language, instead of a third-party templating tool. A fundamental difference between Express running on Node and Ktor running on Kotlin is that the latter is natively concurrent, which is ideal for a request server, in terms of performance. Express is simple to start with: just take an NPM project, add a dependency, and run a single file in the root directory. Setting up Ktor also starts by generating a project, for which we can use the web-based generator. That gives us a project layout with the basic features we want. I mostly went with the defaults but added the HTML DSL: On disk, when unzipped, this gives us a Gradle project. If we run it, it should start listening on port 8080. We can use the included script like so: $./gradle run Starting a Gradle Daemon (subsequent builds will be faster) Now, if you visit localhost:8080, you’ll get a “Hello, World!” back. The route is defined in src/main/kotlin/com/example/plugins/Routing.kt: package com.example.plugins import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.server.routing.* fun Application.configureRouting() { routing { get('/') { call.respondText('Hello World!') } } } This code imports some packages from Ktor, which it uses to define an endpoint. Starting inside, we have the call.respondText() call that sends a text response. This is wrapped by the get() call, defining a GET request at the root path. This is all wrapped by the call to the routing function. If you’re new to Kotlin, the syntax of the call to routing might be surprising, because there are no parentheses. This is a shorthand syntax called passing trailing lambdas, which lets you define a function body directly after the function call that accepts it. Net result: We’re passing the body of the curly braces into the routing function. Our Application.configureRouting sets up a simple route for us. This is a “plugin” for Ktor, allowing us to extend the server with our own routes. It is called by the application’s main setup file at src/main/kotlin/com/example/Application.kt: package com.example import com.example.plugins.* import io.ktor.server.application.* fun main(args: Array) { io.ktor.server.netty.EngineMain.main(args) } fun Application.module() { configureRouting() } The main function is the entry for the whole application; it’s what is run by Gradle or during production, and it starts up a Netty server. The Application.module() function is a lifecycle callback, called by Ktor as part of the application startup process, and that is where we invoke our configureRouting() function. Now, if we want to generate more sophisticated content from that endpoint, like a list of quotes and their authors, we can switch to using Ktor’s HTML DSL. First, we need two more imports, which are already part of the project because we included the DSL in the generated project: import io.ktor.server.html.* import kotlinx.html.* And we can generate our response like so: routing { get('/') { call.respondHtml { head { title('Quotes') } body { h1 { +'Quotes to Live By' } ul { listOf( 'This Mind is the matrix of all matter.' to 'Max Planck', 'All religions, arts and sciences are branches of the same tree.' to 'Albert Einstein', 'The mind is everything. What you think you become.' to 'Buddha' ).forEach { (quote, author) -> li { p { +quote } p { +'― $author' } } } } } This is using the HTML builder functions from Ktor, and it showcases some of the flexibility in Kotlin’s functional syntax. The DSL functions that correspond to HTML tags are readily understandable, and we create the same nested structure we would with plain HTML. In curly braces, we define the content nested inside each tag. It can be more markup, text, variables or some combination. In the unordered list element (ul) we use listOf to create a list on the fly, and the to function to create a pair, a convenience object for holding two values. With that in hand, we dynamically generate an li element using forEach and more DSL. Notice that we can freely blend any valid Kotlin constructs with the functions for HTML tags. This gives us a very powerful setup for generating user interfaces with full access to whatever data or logic we might need. Endpoints and parameters Now let’s say we want to accept a parameter that will filter the quotes by author. We could introduce an endpoint like so: fun Application.configureRouting() { val quotes = mutableListOf( 'This Mind is the matrix of all matter.' to 'Max Planck', 'All religions, arts and sciences are branches of the same tree.' to 'Albert Einstein', 'The mind is everything. What you think you become.' to 'Buddha' ) //... get('/quotes/{author?}') { val author = call.parameters['author'] val filteredQuotes = if (author!= null) { quotes.filter { it.second.equals(author, ignoreCase = true) } } else { quotes } call.respondHtml { head { title('Quotes') } body { h1 { +'Quotes to Live By' } ul { filteredQuotes.forEach { (quote, author) -> li { p { +quote } p { +'― $author' } } } } } } } } We’ve moved the list of quotes into the class body, so it will survive between requests. And inside the get() function, we have separated the logic code out from the markup and defined a filteredQuotes() function. This is so we can dynamically generate the list based on the filter string more cleanly. This example shows the flexibility we have to organize the code and markup as we see fit. Our path for the GET request, /quotes/{author?}, includes a path parameter called author, and we access it in the body of the function like so: call.parameters['author']. The call() method is another example of a “receiver type,” an implicit variable we have access to because we are defining an extension of an existing class. This is an interesting fact about Kotlin and Ktor’s style: it makes for more concise code, but requires that you know about what you are extending and the methods available to you—or use an IDE (like IntelliJ) that has good contextual support to let you know. Thanks to descriptive naming, it’s easy enough to read existing code and divine what’s happening. (Learn more about extension functions and receivers.) Handling a POST The next feature we need to support is submitting and parsing a POST request with a new quote. We can create a simple form like so: get('/add-quote') { call.respondHtml { body { form(method = FormMethod.post, action = '/quotes') { label { +'Quote:' } br {} textInput(name = 'quote') { } br {} label { +'Author:' } br {} textInput(name = 'author') { } br {} submitInput { +'Add Quote' } } } } } Once again, we benefit from the DSL’s harmony with HTML itself: if you can read HTML, you can pretty much read whatever UI this code creates. We have a simple form with two attributes, method (POST) and action (/quotes). Inside it are two fields, author and quote, and a submit button. Now we need to write that /quotes POST endpoint: post('/quotes') { val formParameters = call.receiveParameters() val quote = formParameters['quote']?: '' val author = formParameters['author']?: '' quotes.add(quote to author) call.respondRedirect('/quotes') } The call.receiveParameters() function lets us extract the values from the form body. With those values in hand, we add a new quote to the list and then redirect the client to the /quotes page. Ktor lifecycle methods Another facet we should touch on is what is called middleware in Express but works a little differently in Ktor. Middleware lets us introduce logic that executes across many or all requests. In Ktor, there are several lifecycle methods we can add callbacks to, like onCall. (See the full list of Ktor lifecycle hook points.) Ktor includes a logging plugin that integrates with Log4j, but we can create a simple logger ourselves, like so: val RequestLoggingPlugin = createApplicationPlugin(name = 'RequestLoggingPlugin') { onCall { call -> val scheme = call.request.local.scheme val host = call.request.local.localHost val port = call.request.local.localPort val uri = call.request.local.uri println('Request URL: $scheme://$host:$port$uri') } } Then install it: fun Application.module() { configureRouting() install(RequestLoggingPlugin) // add this } Our plugin extracts the request info from the ApplicationCall object passed into the onCall handler and outputs it to the console. The lifecycle callbacks give you access to whatever aspects of the request processing you might encounter. Conclusion Ktor is a unique take on the HTTP server and benefits from Kotlin’s inherently lean syntax and hybrid programming paradigms. So far we’ve scratched the surface of what we need for building a real application. We’ll continue with this example in my next article, where we will include data persistence and HTMX for a dynamic UI without the reactive JavaScript front end.
https://www.infoworld.com/article/3631092/intro-to-ktor-the-http-server-for-kotlin.html
Related News |
25 sources
Current Date
Jan, Fri 10 - 16:19 CET
|