Guides: How to create an API using ktor

In this guide you will learn how to create an API using ktor.We are going to create a simple API to store simple text snippets (like a small pastebin-like API).

To achieve this, we are going to use the Routing, StatusPages, Authentication, JWT Authentication,CORS, ContentNegotiation and Jackson features.

While many frameworks advocate how to create REST API’s the majority aren’t actually talking about REST APIs but HTTP APIs.Ktor, much like many other frameworks can be used to create systems that comply with REST constraints. However,this tutorial is not talking about REST but HTTP APIs, i.e. endpoints using HTTP verbs that may or may not return JSON, XML or any other format.If you want to learn more about RESTful systems, you can start reading https://en.wikipedia.org/wiki/Representational_state_transfer.

Table of contents:

Setting up the project

The first step is to set up a project. You can follow the Quick Start guide, or use the following form to create one:

the pre-configured generator form))

Simple routing

First of all, we are going to use the routing feature. This feature is part of the Ktor’s core, so you won’t needto include any additional artifacts.

This feature is installed automatically when using the routing { } DSL block.

Let’s start creating a simple GET route that responds with OK by using the get method available inside the routing block:

  1. fun Application.module() {
  2. routing {
  3. get("/snippets") {
  4. call.respondText("OK")
  5. }
  6. }
  7. }

Serving JSON content

A HTTP API usually responds with JSON. You can use the Content Negotiation feature with Jackson for this:

  1. fun Application.module() {
  2. install(ContentNegotiation) {
  3. jackson {
  4. }
  5. }
  6. routing {
  7. // ...
  8. }
  9. }

To respond to a request with a JSON, you have to call the call.respond method with an arbitrary object.

  1. routing {
  2. get("/snippets") {
  3. call.respond(mapOf("OK" to true))
  4. }
  5. }

Now the browser or client should respond to http://127.0.0.1:8080/snippets with {"OK":true}

If you get an error like Response pipeline couldn't transform '…' to the OutgoingContent, check that you haveinstalled the ContentNegotiation feature with Jackson.

You can also use typed objects as part of the reply (but ensure that your classes are not definedinside a function or it won’t work). So for example:

  1. data class Snippet(val text: String)
  2. val snippets = Collections.synchronizedList(mutableListOf(
  3. Snippet("hello"),
  4. Snippet("world")
  5. ))
  6. fun Application.module() {
  7. install(ContentNegotiation) {
  8. jackson {
  9. enable(SerializationFeature.INDENT_OUTPUT) // Pretty Prints the JSON
  10. }
  11. }
  12. routing {
  13. get("/snippets") {
  14. call.respond(mapOf("snippets" to synchronized(snippets) { snippets.toList() }))
  15. }
  16. }
  17. }

Would reply with:

HTTP API - 图1

Handling other HTTP methods

HTTP APIs use most of the HTTP methods/verbs (HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS) to perform operations.Let’s create a route to add new snippets. For this, we will need to read the JSON body of the POST request.For this we will use call.receive<Type>():

  1. data class PostSnippet(val snippet: PostSnippet.Text) {
  2. data class Text(val text: String)
  3. }
  4. // ...
  5. routing {
  6. get("/snippets") {
  7. call.respond(mapOf("snippets" to synchronized(snippets) { snippets.toList() }))
  8. }
  9. post("/snippets") {
  10. val post = call.receive<PostSnippet>()
  11. snippets += Snippet(post.snippet.text)
  12. call.respond(mapOf("OK" to true))
  13. }
  14. }

Now it is time to actually try our backend.

If you have IntelliJ IDEA Ultimate, you can use its built-in powerful HTTP Request client,if not, you can also use postman or curl:

IntelliJ IDEA Ultimate:

IntelliJ IDEA Ultimate, along PhpStorm and other IDEs from JetBrains include avery nice Editor-Based Rest Client.

First you have to create a HTTP Request file (either api or http extensions)HTTP API - 图2

Then you have to type the method, url, headers and payload like this:

HTTP API - 图3

  1. POST http://127.0.0.1:8080/snippets
  2. Content-Type: application/json
  3. {"snippet": {"text" : "mysnippet"}}

And then in the play gutter icon from the URL, you can perform the call, and get the response:

HTTP API - 图4

And that’s it!

This allows you to define files (plain or scratches) that include definition for several HTTP requests,allowing to include headers, provide a payload inline, or from files, use environment variables defined in a JSON file,process the response using JavaScript to perform assertions, or to store some environment variables likeauthentication credentials so they are available to other requests. It supports autocompletion, templates, andautomatic language injection based on Content-Type, including JSON, XML, etc..

In addition to easily test your backends inside your editor, it also helps your to document your APIsby including a file with the endpoints on it.And allows you fetch and locally store responses and visually compare them.

CURL:

Bash:Response:
  1. curl \
  2. request POST \
  3. header "Content-Type: application/json" \
  4. data '{"snippet" : {"text" : "mysnippet"}}' \
  5. http://127.0.0.1:8080/snippets
  1. {
  2. "OK" : true
  3. }

Let’s do the GET request again:

HTTP API - 图5

Nice!

Grouping routes together

Now we have two separate routes that share the path (but not the method) and we don’t want to repeat ourselves.

We can group routes with the same prefix, using the route(path) { } block. For each HTTP method, there is anoverload without the route path argument that we can use at routing leaf nodes:

  1. routing {
  2. route("/snippets") {
  3. get {
  4. call.respond(mapOf("snippets" to synchronized(snippets) { snippets.toList() }))
  5. }
  6. post {
  7. val post = call.receive<PostSnippet>()
  8. snippets += Snippet(post.snippet.text)
  9. call.respond(mapOf("OK" to true))
  10. }
  11. }
  12. }

Authentication

It would be a good idea to prevent everyone from posting snippets. For now, we are going to limit it usinghttp’s basic authentication with a fixed user and password. To do it, we are going to use the authentication feature.

  1. fun Application.module() {
  2. install(Authentication) {
  3. basic {
  4. realm = "myrealm"
  5. validate { if (it.name == "user" && it.password == "password") UserIdPrincipal("user") else null }
  6. }
  7. }
  8. // ...
  9. }

After installing and configuring the feature, we can group some routes together to be authenticated with theauthenticate { } block.

In our case, we are going to keep the get call unauthenticated, and going to require authentication for the post one:

  1. routing {
  2. route("/snippets") {
  3. get {
  4. call.respond(mapOf("snippets" to synchronized(snippets) { snippets.toList() }))
  5. }
  6. authenticate {
  7. post {
  8. val post = call.receive<PostSnippet>()
  9. snippets += Snippet(post.snippet.text)
  10. call.respond(mapOf("OK" to true))
  11. }
  12. }
  13. }
  14. }

JWT Authentication

Instead of using a fixed authentication, we are going to use JWT tokens.

We are going to add a login-register route. That route will register a user if it doesn’t exist,and for a valid login or register it will return a JWT token.The JWT token will hold the user name, and posting will link a snippet to the user.

We will need to install and configure JWT (replacing the basic auth):

  1. open class SimpleJWT(val secret: String) {
  2. private val algorithm = Algorithm.HMAC256(secret)
  3. val verifier = JWT.require(algorithm).build()
  4. fun sign(name: String): String = JWT.create().withClaim("name", name).sign(algorithm)
  5. }
  6. fun Application.module() {
  7. val simpleJwt = SimpleJWT("my-super-secret-for-jwt")
  8. install(Authentication) {
  9. jwt {
  10. verifier(simpleJwt.verifier)
  11. validate {
  12. UserIdPrincipal(it.payload.getClaim("name").asString())
  13. }
  14. }
  15. }
  16. // ...
  17. }

We will also need a data source holding usernames and passwords. One simple option would be:

  1. class User(val name: String, val password: String)
  2. val users = Collections.synchronizedMap(
  3. listOf(User("test", "test"))
  4. .associateBy { it.name }
  5. .toMutableMap()
  6. )
  7. class LoginRegister(val user: String, val password: String)

With all this, we can already create a route for logging or registering users:

  1. routing {
  2. post("/login-register") {
  3. val post = call.receive<LoginRegister>()
  4. val user = users.getOrPut(post.user) { User(post.user, post.password) }
  5. if (user.password != post.password) error("Invalid credentials")
  6. call.respond(mapOf("token" to simpleJwt.sign(user.name)))
  7. }
  8. }

Now we can already try to obtain a JWT token for our user:

Using the Editor-Based HTTP client for IntelliJ IDEA Ultimate,you can make the POST request, and check that the content is valid,and store the token in an environment variable:

HTTP API - 图6

HTTP API - 图7

Now you can make a request using the environment variable {{auth_token}}:

HTTP API - 图8

HTTP API - 图9

If you want to easily test different endpoints in addition to localhost,you can create a http-client.env.json file and put a map with environmentsand variables like this:

HTTP API - 图10

After this, you can start using the user-defined {{host}} env variable:HTTP API - 图11

When trying to run a request, you will be able to choose the environment to use:HTTP API - 图12

Associating users to snippets

Since we are posting snippets with an authenticated route, we have access to the generated Principal that includesthe username. So we should be able to access that user and associate it to the snippet.

First of all, we will need to associate user information to snippets:

  1. data class Snippet(val user: String, val text: String)
  2. val snippets = Collections.synchronizedList(mutableListOf(
  3. Snippet(user = "test", text = "hello"),
  4. Snippet(user = "test", text = "world")
  5. ))

Now we can use the principal information (that is generated by the authentication feature when authenticating JWT)when inserting new snippets:

  1. routing {
  2. // ...
  3. route("/snippets") {
  4. // ...
  5. authenticate {
  6. post {
  7. val post = call.receive<PostSnippet>()
  8. val principal = call.principal<UserIdPrincipal>() ?: error("No principal")
  9. snippets += Snippet(principal.name, post.snippet.text)
  10. call.respond(mapOf("OK" to true))
  11. }
  12. }
  13. }
  14. }

Let’s try this:

HTTP API - 图13

HTTP API - 图14

Awesome!

StatusPages

Now let’s refine things a bit. A HTTP API should use HTTP Status codes to provide semantic information about errors.Right now, when an exception is thrown (for example when trying to get a JWT token from an user that already exists,but with a wrong password), a 500 server error is returned. We can do it better, and the StatusPages featureswill allow you to do this by capturing specific exceptions and generating the result.

Let’s create a new exception type:

  1. class InvalidCredentialsException(message: String) : RuntimeException(message)

Now, let’s install the StatusPages feature, register this exception type, and generate an Unauthorized page:

  1. fun Application.module() {
  2. install(StatusPages) {
  3. exception<InvalidCredentialsException> { exception ->
  4. call.respond(HttpStatusCode.Unauthorized, mapOf("OK" to false, "error" to (exception.message ?: "")))
  5. }
  6. }
  7. // ...
  8. }

We should also update our login-register page to throw this exception:

  1. routing {
  2. post("/login-register") {
  3. val post = call.receive<LoginRegister>()
  4. val user = users.getOrPut(post.user) { User(post.user, post.password) }
  5. if (user.password != post.password) throw InvalidCredentialsException("Invalid credentials")
  6. call.respond(mapOf("token" to simpleJwt.sign(user.name)))
  7. }
  8. }

Let’s try this:

HTTP API - 图15

HTTP API - 图16

Things are getting better!

CORS

Now suppose we need this API to be accessible via JavaScript from another domain. We will need to configure CORS.And Ktor has a feature to configure this:

  1. fun Application.module() {
  2. install(CORS) {
  3. method(HttpMethod.Options)
  4. method(HttpMethod.Get)
  5. method(HttpMethod.Post)
  6. method(HttpMethod.Put)
  7. method(HttpMethod.Delete)
  8. method(HttpMethod.Patch)
  9. header(HttpHeaders.Authorization)
  10. allowCredentials = true
  11. anyHost()
  12. }
  13. // ...
  14. }

Now our API is accessible from any host :)

Full Source

application.kt

  1. package com.example
  2. import com.auth0.jwt.*
  3. import com.auth0.jwt.algorithms.*
  4. import com.fasterxml.jackson.databind.*
  5. import io.ktor.application.*
  6. import io.ktor.auth.*
  7. import io.ktor.auth.jwt.*
  8. import io.ktor.features.*
  9. import io.ktor.http.*
  10. import io.ktor.jackson.*
  11. import io.ktor.request.*
  12. import io.ktor.response.*
  13. import io.ktor.routing.*
  14. import java.util.*
  15. fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
  16. fun Application.module() {
  17. val simpleJwt = SimpleJWT("my-super-secret-for-jwt")
  18. install(CORS) {
  19. method(HttpMethod.Options)
  20. method(HttpMethod.Get)
  21. method(HttpMethod.Post)
  22. method(HttpMethod.Put)
  23. method(HttpMethod.Delete)
  24. method(HttpMethod.Patch)
  25. header(HttpHeaders.Authorization)
  26. allowCredentials = true
  27. anyHost()
  28. }
  29. install(StatusPages) {
  30. exception<InvalidCredentialsException> { exception ->
  31. call.respond(HttpStatusCode.Unauthorized, mapOf("OK" to false, "error" to (exception.message ?: "")))
  32. }
  33. }
  34. install(Authentication) {
  35. jwt {
  36. verifier(simpleJwt.verifier)
  37. validate {
  38. UserIdPrincipal(it.payload.getClaim("name").asString())
  39. }
  40. }
  41. }
  42. install(ContentNegotiation) {
  43. jackson {
  44. enable(SerializationFeature.INDENT_OUTPUT) // Pretty Prints the JSON
  45. }
  46. }
  47. routing {
  48. post("/login-register") {
  49. val post = call.receive<LoginRegister>()
  50. val user = users.getOrPut(post.user) { User(post.user, post.password) }
  51. if (user.password != post.password) throw InvalidCredentialsException("Invalid credentials")
  52. call.respond(mapOf("token" to simpleJwt.sign(user.name)))
  53. }
  54. route("/snippets") {
  55. get {
  56. call.respond(mapOf("snippets" to synchronized(snippets) { snippets.toList() }))
  57. }
  58. authenticate {
  59. post {
  60. val post = call.receive<PostSnippet>()
  61. val principal = call.principal<UserIdPrincipal>() ?: error("No principal")
  62. snippets += Snippet(principal.name, post.snippet.text)
  63. call.respond(mapOf("OK" to true))
  64. }
  65. }
  66. }
  67. }
  68. }
  69. data class PostSnippet(val snippet: PostSnippet.Text) {
  70. data class Text(val text: String)
  71. }
  72. data class Snippet(val user: String, val text: String)
  73. val snippets = Collections.synchronizedList(mutableListOf(
  74. Snippet(user = "test", text = "hello"),
  75. Snippet(user = "test", text = "world")
  76. ))
  77. open class SimpleJWT(val secret: String) {
  78. private val algorithm = Algorithm.HMAC256(secret)
  79. val verifier = JWT.require(algorithm).build()
  80. fun sign(name: String): String = JWT.create().withClaim("name", name).sign(algorithm)
  81. }
  82. class User(val name: String, val password: String)
  83. val users = Collections.synchronizedMap(
  84. listOf(User("test", "test"))
  85. .associateBy { it.name }
  86. .toMutableMap()
  87. )
  88. class InvalidCredentialsException(message: String) : RuntimeException(message)
  89. class LoginRegister(val user: String, val password: String)

my-api.http

  1. # Get all the snippets
  2. GET {{host}}/snippets
  3. ###
  4. # Register a new user
  5. POST {{host}}/login-register
  6. Content-Type: application/json
  7. {"user" : "test", "password" : "test"}
  8. > {%
  9. client.assert(typeof response.body.token !== "undefined", "No token returned");
  10. client.global.set("auth_token", response.body.token);
  11. %}
  12. ###
  13. # Put a new snippet (requires registering)
  14. POST {{host}}/snippets
  15. Content-Type: application/json
  16. Authorization: Bearer {{auth_token}}
  17. {"snippet" : {"text": "hello-world-jwt"}}
  18. ###
  19. # Try a bad login-register
  20. POST http://127.0.0.1:8080/login-register
  21. Content-Type: application/json
  22. {"user" : "test", "password" : "invalid-password"}
  23. ###

http-client.env.json

  1. {
  2. "localhost": {
  3. "host": "http://127.0.0.1:8080"
  4. },
  5. "prod": {
  6. "host": "https://my.domain.com"
  7. }
  8. }

Exercises

After following this guide, as an exercise, you can try to do the following exercises:

Exercise 1

Add unique ids to each snippet and add a DELETE http verb to /snippets allowing an authenticated user to deleteher snippets.

Exercise 2

Store users and snippets in a database.