In this three-part series, I will demonstrate how to create a simple full stack web app in Kotlin. The result is an app that allows a user to download and tag random inspirational images and then search for images by tag.
Part 1: Developing the backend with Ktor
Part 3: Developing the frontend with React and Kotlin/JS
API
We will be building the following API:
1. To get a new inspiration:
POST
http://{{base_url}}/users/{{user_id}}/inspirations
2. To change the tags for an existing inspiration:
PATCH
http://{{base_url}}/users/{{user_id}}/inspirations/{{inspiration_id}}
3. To get an existing inspiration image:
GET
http://{{base_url}}/users/{{user_id}}/inspirations/{{inspiration_id}}/images
4. To find a tag by title:
GET
http://{{base_url}}/tags?title={{any_title}}
5. To find an inspiration by tag
GET
http://{{base_url}}/users/{{user_id}}/inspirations?tagId={{tag_id}}
Setup
This tutorial requires IntelliJ IDEA and the Ktor plugin.
1. Create a new project using the New Project dialog
2. Select Ktor as project category, and tick the CORS and Routing features, and GSON as content negotiation
3. Define the project metadata
4. Specify where to save the project, and finish the setup
5. Add the following dependencies to your build.gradle file and sync Gradle
Ktor
Ktor is a JetBrains framework for building asynchronous clients and servers in Kotlin. Ktor supports many types of clients, including JVM and JavaScript, but only JVM servers at the moment. Ktor is built from the ground up on coroutines, which allows for asynchronous code to be written in a very natural, almost synchronous style. Coroutines are also highly performant, outstripping traditional threads by far. Ktor provides a DSL (or domain-specific language) for writing server code, making the application code very concise and readable.
Application
Ktor application structure has a pyramid shape, in that the application is built upon user-defined modules, which is in turn built upon features.
The application's primary functions are to create a server for handling requests and to configure the server with a module. The code snippet below shows all that is needed to accomplish this. First, an embedded server is created with the Netty application engine and instructed to listen to port 8080 for incoming requests. It is also configured with a module, the extension function Application.inspirot, which we will examine shortly. Lastly, the server is started and instructed to wait indefinitely for incoming requests.
Module
The primary function of a Ktor module is to specify application behaviour by installing and configuring features. The code snippet below shows how to install the ContentNegotiation feature and configure it to handle JSON serialisation using the GSON library.
Arguably the most important feature to be installed and configured relates to routing, that is - mapping a request to code that produces an appropriate response. Since routing is such a commonly used feature, it comes pre-installed and only needs to be configured. The code snippet below shows the skeleton structure of the application’s routing configuration. Each numbered route corresponds to the API method mentioned above.
One of the most important routes is that of creating an inspiration (1). Let’s examine the implementation of this route.
The route is implemented in five steps:
Getting the userId and checking that it is present and of the right type
Getting the image URL from the image source website
Getting the actual image from the image source website
Saving the inspiration and its associated image to the database and filesystem
Responding to the client with the created inspiration
Let’s examine each step more closely.
Step 1: Notice the variable call? Whenever a request is sent from the client, the server transforms the request to an ApplicationCall object, simply referred to as call in the DSL. Path variables and query parameters can be obtained from call, and so the code gets path variable userId, and then checks if it is of the appropriate type. Should it not be, the client had made a mistake, necessitating a Bad Request response. Nothing further can be done, and the code exits out of the block using return@post.
Step 2: Now we need to get the image URL from the image source website. I have abstracted the exact details of how this is done behind an interface called ApiRepository, but we will see the implementation later on in this post. If the retrieval of an image URL was unsuccessful, it necessitates a Internal Server Error response. Nothing further can be done, and the code exits out of the block using return@post.
Step 3: Next, we need to get the actual image from the image source website. Once again I have abstracted the exact details of how this was done.
Step 4. Almost done! The inspiration must be saved to the database and the image to the file system. Once again I have abstracted the exact details of how this is done behind an interface called DBRepository. We will examine the implementation of saving to the database a bit later when we work with Exposed. For now, notice the use of named arguments to make the arguments’ meaning clear. If saving was unsuccessful, it necessitates an Internal Server Error response. Nothing further can be done, and the code exits out of the block using return@post.
Step 5: Success! All that needs to be done now is respond to the client with a Created status code and the inspiration we created, and we’re done.
The call object is fundamental to interacting with the request and sending a response. Let’s summarise some of it’s most critical features:
Request:
To get a path variable, use call.parameter[“name”]
To get a query parameter, use call.parameter[“name”]
To get something from the body of the request, use call.receive<type>() parameterising it with the expected body type
Response:
To respond with simple text, use call.respondText
To respond with a status code and an object, use call.respond(statusCode, object)
To respond with a file, use call.respondFile(file)
Demo
Catch the next part of Step-by-Step Full Stack Kotlin (Part 2) next week!
With thanks for Annyce Davis for her feedback.
I'm not fully understand what is Application::inspirobot. In fact, i cannot import it. "Unsolved reference: inspirobot"