1. Introduction

In this article, we’re going to look at creating a simple microservice in Kotlin using the Spark framework.

Our example API is going to be the basis for a straightforward social network, where we can submit and query posts and then adjust the number of likes that a post has.

2. Getting Started

Before we can start, we need to ensure that we have the appropriate dependencies. To begin with, we need to have Spark available:

<dependency>
    <groupId>com.sparkjava</groupId>
    <artifactId>spark-kotlin</artifactId>
    <version>1.0.0-alpha</version>
</dependency>
<dependency>
    <groupId>com.sparkjava</groupId>
    <artifactId>spark-core</artifactId>
    <version>2.9.4</version>
</dependency>

Note that the spark-kotlin dependency will pull spark-core in transitively, but it’ll pull in an older version, so we want to make sure we have the latest version to use.

We’re also going to need Jackson to support processing JSON requests and responses:

<dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-kotlin</artifactId>
    <version>2.15.3</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.15.3</version>
</dependency>

We’ll also create our main() function to run our service. For now, we won’t do much here, but we’ll expand it as we go:

fun main() {
    val objectMapper = jacksonObjectMapper()
    objectMapper.registerModule(JavaTimeModule())
    objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
}

All we’re doing is creating a Jackson ObjectMapper that we’re going to want to use later on.

3. Model and Repository

Before we can start writing our service, we need to have some data to work with.

Let’s start by defining our model object. For our API, we’re going to have a very simple data class to represent a post:

data class Post(
    val id: String,
    val posterId: String,
    val posted: Instant,

    val content: String,
    val likes: Int
)

We’re also going to have a simple HitList type to represent a subset of posts within our data:

data class HitList<T>(
    val entries: List<T>,
    val total: Int
)

We can now create our PostRepository class. This will be mainly empty to start with, and we’ll expand it as we add functionality to our API:

class PostRepository {
    private val data = mutableListOf<Post>()

    fun create(posterId: String, content: String) : Post {
        val newPost = Post(
          id = UUID.randomUUID().toString(),
          posterId = posterId,
          posted = Instant.now(),

          content = content,
          likes = 0
        )

        data.add(newPost)

        return newPost
    }
}

We’ve added a create() method already because we’re going to want that to set up our initial data.

Note that we’re using an in-memory list here to represent our data. In reality, we’d use a real database, but this is all abstracted behind the repository anyway, so the rest of the application doesn’t need to care.

Finally, let’s create an instance of our repository in our main method and give it some initial data:

val repository = PostRepository()
println(repository.create("1", "This is my first post"))
println(repository.create("1", "And a second one"))
println(repository.create("2", "Hello, World!"))

Note that we’re wrapping the result of our create() calls in a println() call. This is simply so that we can see on startup what the generated IDs for these posts are so that we can interact with them.

4. Getting Posts by ID

Now that we have our repository let’s actually interact with it. We’ll start by adding an HTTP handler to get individual posts by their unique ID.

Firstly, we need a method in our Repository to get the posts by ID:

fun getById(id: String) : Post? {
    return data.find { it.id == id }
}

This is very simple. We just find the first entry in our data list with the correct ID and return it, which will also return null if nothing is found.

Now we’re ready to write our first handler:

Spark.get("/posts/:id", { req, res ->
    val id = req.params("id")
    val post = repository.getById(id)

    if (post == null) {
        res.status(404)
    }

    post
}, objectMapper::writeValueAsString)

This is all it takes to get our service working. This handler will handle all requests to GET /posts/:id and will:

  • Get the ID of the post that we want to retrieve from the appropriate path parameter.
  • Look up the post in our repository.
  • If the post wasn’t found, return an HTTP 404 Not Found.
  • Otherwise, return the post data as-is.

Typically, it’s bad practice to use our database model directly on our APIs, but for the sake of this article, it’s good enough.

We’re also providing the objectMapper::writeValueAsString function as our response transformer. This ensures that the output is transformed into JSON for the client.

If we run our service now, we can send an HTTP request and get back post data:

GET /posts/ecc4c9a9-b4af-4da7-8353-f0274a6e65bb HTTP/1.1
Host: localhost:4567

HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8

{
    "content": "Hello, World!",
    "id": "ecc4c9a9-b4af-4da7-8353-f0274a6e65bb",
    "likes": 0,
    "posted": "2023-11-08T09:27:40.949917Z",
    "posterId": "2"
}

However, there’s a problem here. The service is returning our post data, but it claims it’s text/html. That’s clearly wrong! So, we need to tell Spark that the content type is application/json. We can do this in our handler, but since we’re going to want to do this in every handler, we’ll instead write a before() filter that does it on every single call:

Spark.before({ _, res -> res.type("application/json") })

Now we’re working correctly. Calls to get a post by ID will return the post data, correctly formatted as JSON, or else will return an HTTP 404 Not Found if the ID is unknown.

5. Listing Posts

Now that we can get individual posts, what about getting all of them? In particular, we want to have two different ways to do this – either getting all of the posts in the entire system or all of the posts filtered by a particular poster. We also want to paginate the responses so the client doesn’t overwhelm the service.

Firstly, we’ll write a simple helper method for safely taking sublists. This makes doing pagination a bit easier:

private fun <T> List<T>.safeSubList(fromIndex: Int, toIndex: Int): List<T> =
    this.subList(fromIndex.coerceAtLeast(0), toIndex.coerceAtMost(this.size))

This is an extension method for the standard List type, allowing us to take a sublist without first ensuring it doesn’t go past the end of the list.

Now, we’ll add a method to our PostRepository class to get a page of the entire set of posts:

fun getAll(offset: Int, count: Int) : HitList<Post> {
    val page = data
      .sortedWith(compareByDescending { it.posted })
      .safeSubList(offset, offset + count)

    return HitList(entries = page, total = data.size)
}

This is implicitly sorted by the date the post was created and uses our safeSubList() extension method to give us our desired page.

Next, we’ll do one but filtering by the poster:

fun getForPoster(posterId: String, offset: Int, count: Int) : HitList<Post> {
    val total = data.count { it.posterId == posterId }
    val page = data
      .sortedWith(compareByDescending { it.posted })
      .filter { it.posterId == posterId }
      .safeSubList(offset, offset + count)

    return HitList(entries = page, total = total)
}

Here, we filter the data before paginating it, but we calculate the total count with a filter instead of just taking the entire list size.

Now we can write our handler. This will use three query parameters, all of which are optional, and call the correct repository method. It’ll then simply return the retrieved data:

Spark.get("/posts", { req, _ ->
    val posterId = req.queryParams("posterId")

    val offset = req.queryParams("offset")?.toIntOrNull() ?: 0
    val count = req.queryParams("count")?.toIntOrNull() ?: 10

    if (posterId == null) {
        repository.getAll(offset, count)
    } else {
        repository.getForPoster(posterId, offset, count)
    }
}, objectMapper::writeValueAsString)

Because of the before() filter we added before, we’re already setting the correct content-type header, so this is all it takes to query our data.

6. Creating Posts

Next, we’ll look at how to create new posts with our API. We’ve already got the appropriate method on our PostRepository, so we only need to write our handler.

Creating a new post will be done by doing an HTTP POST where the request body contains the post content and the poster ID. As such, the first thing we’ll need is a class to represent this:

data class CreatePostRequest(val posterId: String, val content: String)

Now we can write our handler. We’ll use our ObjectMapper to parse the request body and then just call our repository with the values:

Spark.post("/posts", { req, res ->
    val body = objectMapper.readValue<CreatePostRequest>(req.bodyAsBytes())

    val post = repository.create(body.posterId, body.content)

    res.status(201)
    res.header("Location", "/posts/${post.id}")
    
    post
}, objectMapper::writeValueAsString)

We’re also setting the HTTP status code to HTTP 201 Created and providing a Location header to the canonical URL for the new post.

7. Deleting Posts

Now we can create posts, what about deleting them? This will be done using the HTTP DELETE method.

Firstly, we need a repository method. This does nothing more than remove the post that has the given ID:

fun deleteById(id: String) {
    data.removeIf { it.id == id }
}

If the ID doesn’t exist, that’s fine – we can consider that a success just as much as if it did exist. After all, the result is the same. This helps ensure that the API is idempotent – multiple requests to delete the same ID end up with the same state.

Now, we’ll add our handler method. This is very similar to the one for getting a post, only using a different handler method and not caring if the post existed or not:

Spark.delete("/posts/:id") { req, res ->
    val id = req.params("id")

    repository.deleteById(id)

    res.status(204)
}

8. Updating Posts

The final thing we need to do is to update the likes on a post. Because we’re only updating a single field, we’ll do this with an HTTP PATCH instead of a PUT. We’re also going to use JSON Merge Patch since it’s the simplest way to achieve this. All that’s needed is for the client to send a JSON document with a single field – “likes” – with the new desired value.

As always, we’ll start with the repository method. This will be targetted to our exact needs, rather than being generic:

fun updateLikes(id: String, likes: Int): Post {
    val existing = data.find { it.id == id }!!

    data.remove(existing)

    val newPost = existing.copy(likes = likes)
    data.add(newPost)

    return newPost
}

Because our data objects are immutable, this will remove the existing entry from the list and insert a copy, updating the likes field to the desired value. If we were using a real database, then this could just be a direct UPDATE statement.

Now for our handler. As before, we need a class to represent our request payload:

data class PatchPostRequest(val likes: Int)

We don’t need the post ID in this because we’re going to get it from the URL path.

Now we can write the actual handler:

Spark.patch("/posts/:id", { req, res ->
    val id = req.params("id")

    val body = objectMapper.readValue<PatchPostRequest>(req.bodyAsBytes())

    repository.updateLikes(id, body.likes)
}, objectMapper::writeValueAsString)

We can now handle our requests to update the likes on a post:

PATCH /posts/3a10160d-df0e-4113-af36-5545d3beb589 HTTP/1.1
Content-Length: 14
Content-Type: application/json
Host: localhost:4567

{
    "likes": "1"
}

HTTP/1.1 200 OK
Content-Type: application/json

{
    "content": "Hello, World!",
    "id": "3a10160d-df0e-4113-af36-5545d3beb589",
    "likes": 1,
    "posted": "2023-11-08T13:26:19.322158Z",
    "posterId": "2"
}

9. Summary

Here, we’ve seen a brief introduction to building an HTTP API with Spark. This is only scratching the surface of what Spark can do for us, but hopefully, we’ve seen how easy and powerful it can be to work with.

As always, all of the code from this article is available over on GitHub.

Comments are closed on this article!