Ktor 1.0 - Revisiting our Christmas list service
Last Christmas I looked into a beta version of the Ktor framework and used it to create a really simple restful API. The API allowed you to create "users", "wishlists" and "wishlist items". You can find the project on github and read the accompanying post here.
It's Christmas time again and Ktor recently hit 1.0 so I thought now would be a good time to revisit the project to update it and add a a really basic front end to test drive Ktor's web page rendering capabilities.
Let's take a look at the changes we need to make to update the project to 1.0 before we dive into creating a front end. At the time of this writing the latest version was 1.0.1 so let's go to our build.gradle file and update that first
buildscript {
ext.kotlin_version = '1.3.10'
ext.ktor_version = '1.0.1'
}
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
compile "io.ktor:ktor-server-core:$ktor_version"
compile "io.ktor:ktor-server-netty:$ktor_version"
compile "io.ktor:ktor-locations:$ktor_version"
compile "io.ktor:ktor-gson:$ktor_version"
}
Once we have the latest version of the project we'll need to update all of our @location
annotations to @Location
. We'll also eliminate our Route.delete definition. We had originally done this last year because the framework's location API didn't support the DELETE verb. That's no longer true and so we can just use the framework's implementation.
With that relatively painless migration complete we can now move forward with adding some new functionality.
We'll create our front end using the same Locations API that we used when we created our rest end points, but we'll use Freemarker templates to build our html pages. Ktor provides a nice integration with Freemarker via this dependency:
compile "io.ktor:ktor-freemarker:$ktor_version"
Once we have that dependency we can go to the Application class and add these lines to the code that sets up our pipeline:
install(Freemarker){
templateLoader = ClassTemplateLoader(this::class.java.classLoader, "templates")
}
This will let the framework know we want to use the Freemarker library and where it can find our template files. In our project's particular case we are going to store our template files in the templates
directory which we'll need to create underneath the resources directory. We'll circle back to these template files in a moment but for now while we are in the Application.kt file we will also go ahead and add two new locations to our app. These two "locations" will be the pages that we serve to the user.
@Location("/")
class UserPage()
@Location("/users/{userId}/christmaslist")
data class ChristmasListPage(val userId: Long)
This works exactly the same way it did when we were defining our rest end points. We'll have a page at the root of the site where you will be able to add users and then each user will have a christmaslist page where you can go and add items to their Christmas wishlist.
Once we have those locations defined we can create the Route declarations that we can then install in the routing section of our pipeline in Application.kt
Let's look at the Route declaration for our "index" page
fun Route.userPage(userDao: UserDao, wishListDao: WishListDao){
get<UserPage>{
call.respond(FreeMarkerContent("index.ftl", mapOf("model" to IndexPageModel(userDao.getAllUsers()))))
}
post<UserPage> {
val formParams = call.receiveParameters()
if(formParams["name"] != null && formParams["isNice"] != null){
//success
val createdUser = userDao.createUser(formParams["name"]!!, formParams["isNice"]!!.toBoolean())
wishListDao.createChristmasWishList(createdUser.id)
call.respond(FreeMarkerContent("index.ftl", mapOf("model" to IndexPageModel(userDao.getAllUsers()))))
} else {
call.respond(FreeMarkerContent("index.ftl", mapOf("model" to IndexPageModel(userDao.getAllUsers(), "Invalid user data entered"))))
}
}
}
We will need our userDao and wishlistDao to handle the requests for this page so we make sure those are passed into the function we've named "userPage" off of the Route object. Our get
block will handle GET requests. This is where Freemarker will come into play again. We will use call.respond
to send a FreemarkerContent object declaring which template file we want to load along with a map of objects that we use in the template file.
Freemarker will use this map of objects in combination with our template file to render the page. This IndexPageModel object is a simple data class that will hold a list of all users in the system and a nullable error message.
data class IndexPageModel(val users: List<User> = emptyList(),
val error: String? = null)
Our post
block will handle the POST requests that come from our page's form. It will do a simple check to make sure all of the necessary form parameters are in place and then either add the new user to the system via the userDao and create a christmas list for that user or respond with an error message letting the user know they've entered their data incorrectly.
There is one important thing to call out when creating these get
and post
blocks. We are making use of the Ktor locations API which is designed to give you type safe route handling and so we want to make sure that we import io.ktor.locations.get
and io.ktor.locations.post
in our UsersPage kt file. The IDE might try to import io.ktor.routing.post
if you aren't paying close attention and that isn't what we want. You'll want to keep this in mind through out your project if you've decided to use the locations API in your project.
Let's take a look at the index.ftl file now to see how it will make use of the IndexPageModel
object that we've placed our FreeMarkerContent object's map under the model
key.
<html>
<head>
<title>Ktor Christmas List!</title>
</head>
<style>
.container{
display:flex;
flex-wrap:wrap;
text-align:center;
justify-content: center;
}
</style>
<body>
<div class="container">
<#-- @ftlvariable name="model" type="com.abnormallydriven.christmaslistservice.IndexPageModel" -->
<div style="width: 100%;">
<#if model.error??>
<h1>Invalid User Data!</h1>
</#if>
<#if model.users?has_content>
<h1>Christmas List Users</h1>
</#if>
<#list model.users as user>
<p>${user.name} ${user.isNiceString()} <#if user.isNice()><a href="users/${user.id}/christmaslist">View WishList</a></#if></p>
<#else>
<h1>You should add some users</h1>
</#list>
</div>
<form action="/" method="post" enctype="application/x-www-form-urlencoded" style="border: black 1px solid; padding: 10px;">
<div>Name:</div>
<div><input type="text" name="name"/></div>
<div>Is Nice?:</div>
<div>
<input type="radio" name="isNice" value="true" checked>True<br>
<input type="radio" name="isNice" value="false">False<br>
</div>
<div><input type="submit" value="Add"/></div>
</form>
</div>
</body>
</html>
Our html and css won't be winning design awards anytime soon but it's simple and straight forward to illustrate how we can use a Freemarker template file with Ktor. Even if you've never made use of Freemarker before it should be pretty straightforward.
The comment at the top of the div inside of the body tag is just there to help our IDE (IntelliJ IDEA) give us some auto completion while we work in the file. The rest of the file is just some simple Freemarker template elements that check for whether our model has an error message, or if our model's user list has items in it. It will then conditionally render the elements in the template.
The form at the bottom of the body is where we can enter a new user's data. When we submit the form we'll hit our post
block in the userPage
Route function and proceed to add that user as described above. The response will then cause the page to re-render with a new IndexPageModel. This new model will include their new user object.
The last step to get this page into our route pipeline and served to the user when they hit the root of the site will be to install in the Application.kt file's pipeline builder code:
install(Routing) {
userPage(userDao, wishListDao)
//more routes below
}
That's all there is to installing and using Freemarker in your Ktor projects. Our page for adding items to a user's christmas list will follow a very similar pattern. We'll just need to define a new model, template file and route handling. You can check it out in the completed project here on github.
That's all I have for this post. I've included links below that I found useful while updating the project and writing this post.
Have a Merry Christmas and a Happy New Year!
Helpful Links
Ktor
Locations API
Ktor website tutorial
Freemarker
Christmas Wishlist Service