Quarkus Web Application with SvelteJS - Part 1

Part 1 - The Quarkus Backend

Welcome

This will be a 3 part series demonstrating how to build a Quarkus application, using Kotlin. In part 3, we will put a front end on to our application, which will be written in SvelteJS.

This blog series will be broken down into the following sections.

  1. Setting up and building a basic ToDo List application (no database) with tests.
  2. Add a database (postgres), and in memory test DB (H2), db migrations (flyway)
  3. Add a front end using SvelteJS

Why Quarkus?

I am going to assume you already know what Quarkus is. If you don’t, head over to the Quarkus.io homepage and have a read. It does a far better job at describing why it exists than I can do here, but for my reasons, I was looking to find a web framework that had the same enjoyment factor as Playframework 1.x, but with a Cloud / Container first approach, with a target for a super-small memory footprint to limit cost of running webservices/microservices in the cloud. Safe to say, with Quarkus’s live reloading capability and super-fast start-up times (and more), it meets my playframework tick. With native compilation using the GraalVM target, the memory footprint is impressively low and can easily run in docker, giving me my second requirement fulfilled.

Let’s get started

To get started with Quarkus, all you need installed is Java and Maven.

GraalVM is optional and I am not going to cover that here. There are plenty of tutorials around, but if you are using linux or linux subsystem on windows, checkout sdkman for easy setup.

To get started, we are going to use a maven archetype to create the template for our application.

mvn io.quarkus:quarkus-maven-plugin:1.1.1.Final:create -DprojectGroupId=codemwnci -DprojectArtifactId=todo-rest-demo -DclassName="codemwnci.TodoService" -Dpath="/todos" -Dextensions="kotlin,resteasy-jackson"

So, we have chosen the Quarkus maven plugin 1.1.1.Final:create archetype. The projectArtifactId will be the name of the directory created, and the classname will be the name of the Kotlin file created that will contain the initial demo code, with a Rest path accessed via “/todos”. Two extensions have been chosen, firstly Kotlin (which will make sure the maven structure is kotlin specific and the TodoService class is a Kotlin file. We will also include resteasy-jackson, rather than using jsonb. (Note: in previous versions we would have had to also add kotlin-jackson to our POM before running, but this is now included from 1.1.1 onwards).

Once you have run the command, the project should be created, and we are ready to start.

If you just want to see the default app running, you can do the following

cd todo-rest-demo
mvn compile quarkus:dev

This will run the application in dev mode. Go to http://localhost:8080/todos to see the Hello world greeting.

The Todo App

Let’s start with the following requirements for our ToDo App.

  1. List the todos
  2. Retrieve a single todo
  3. Create a todo
  4. Delete a todo
  5. Update a todo (mark as completed)

So this will translate to a typical REST service with the following REST endpoints

  1. GET /todos
  2. GET /todos/{id}
  3. POST /todos
  4. DELETE /todos/{id}
  5. PUT /todos/{id}

The ToDo itself will be represented by an entity that contains

  • ID: Long
  • txt: String
  • completed: Boolean

We now have everything we need to construct our application.

Let’s start with the Todo entity, and the mechanisms to save to a datastore (in this section represented by an arraylist until replaced by a DB in the next section).

Open the file src/main/kotlin/codemwnci/TodoService

First off, delete the hello world function.

Just below the TodoService class definition, add the following code

val idSeq: AtomicLong = AtomicLong()
fun createTodo(txt: String): Todo = Todo(idSeq.incrementAndGet(), txt)
val todos: ArrayList<Todo> = ArrayList()

Also add the following import

import java.util.concurrent.atomic.AtomicLong;

We now have an arraylist we can interact with for our REST endpoints, and a createTodo function that will ensure we generate sequential Ids (in place of not having a DB that would otherwise do the job for us). Apart from the Todo class, this code will be removed when we create our database.

NOTE: having default values, and the properties marked as VAR may look odd. I typically would mark as VAL and only have default values for completed (because I would want it to default to false), but there is a bug in the native compiler that causes Jackson JSON parsing to fail if not configured in this way. If you are using a version greater than 1.1.1.Final, check https://github.com/quarkusio/quarkus/issues/3954 to see if the bug has been fixed.

Next step will be to implement the different endpoints.

Quarkus identifies endpoints using standard JAX-RS annotations. Let’s take our requirements one at a time. Place the following code underneath the arraylist we created above.

Requirement 1: List all Todos

@GET
@Produces(MediaType.APPLICATION_JSON)
fun getAll() = todos

Hopefully this is fully self-explanatory. The function getAll -the function name doesn’t matter, but its useful to be descriptive- simply returns the list of todos. It responds via a GET on the root path of the service (/todos is set up at the TodoService Class level by the @Path annotation. This was done automatically by the maven archetype).

The produced annotation specifies we are using JSON, and therefore Jackson is used to automatically serialise the list of Todos into a JSON string.

Requirement 2: Get a single todo

@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
fun getOne(@PathParam("id") id: Long) = if (!todos.isEmpty()) todos.first { it.id == id } else null

This is slightly more complex, but not much. Again we are using a GET request, but we are looking for an ID in the path, e.g. /todos/1. We pass the path parameter into the function as a parameter, by specifying the PathParam annotation, and then returning the first Todo in the list where the passed in ID matches (if you are new to Kotlin, it is the variable used in lambda functions when there is only a single parameter, and the parameter is otherwise omitted, it is equivalent of writing todos.first {item: Todo -> item.id == id}). The if statement check is to prevent NoSuchElementException being thrown if first is called on an empty list.

Requirement 3: Create a Todo

@POST
@Produces(MediaType.APPLICATION_JSON)
fun addOne(txt: String): Todo {
   val todo = createTodo(txt)
   todos.add(todo)
   return todo
}

This again is pretty straightforward. The text of the Todo is passed in the body of the Todo. If the body was a JSON object, it would be converted into the target object (we’ll see that in requirement 5), but as we are accepting just plain text, then no conversion is needed nor do we need any annotations.

We use the txt value to create a Todo using the createTodo function we wrote at the start of the tutorial (this will ensure we have a sequential ID), then add the Todo to the list, and return the Todo as the output. The return will automatically convert the Todo to a JSON string, with the content encoding set as Application-Json, based onthe @Produces annotation.

Requirement 4: Delete a Todo

@DELETE
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
fun deleteOne(@PathParam("id") id: Long) = todos.remove( todos.first { it.id == id } )

This function is almost the same as the GET function we created for requirement 2. We use @DELETE instead of @GET, but we use the same path, path param, and we find the todo from the list using the ArrayList.first function. From the Todo retrieved from the list, this is passed to the ArrayList.remove() function to remove it from the list.

Requirement 5: Update Todo

@PUT
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
fun updateOne(@PathParam("id") id: Long, todo: Todo): Todo? {
   deleteOne(id)
   todos.add(todo)
   return todo
}

This time we are using @PUT, and passing in both a PathParam and JSON body. The JSON body is automatically converted by Jackson from the String body to the Todo object.

We use the path parameter to then call our deleteOne function, then add the updated Todo back to the list, and finally returning the Todo as output.

Manually Testing our Service using cURL

To manually test our application, if it isn’t already started, run the application with mvn quarkus:dev.

We can then use cURL to test our rest endpoints. There are other options you can use, like Postman, but cURL is nice and simple. If you are running on Windows (like me), I tend to use the Windows Subsystem for Linux, by installing Ubuntu from the Windows Store, giving you access to cURL and much much more.

Let’s take a look at our 5 requirements again, and then the cURL commands we need to test each one.

Req#1: List All

curl [http://localhost:8080/todos](http://localhost:8080/todos)

Req#2: Get One

curl [http://localhost:8080/todos/1](http://localhost:8080/todos/1)

Req#3: Add One

curl -d 'my todo' -H "Content-Type: application/json" -X POST [http://localhost:8080/todos](http://localhost:8080/todos)

Req#4: Delete

curl -X DELETE [http://localhost:8080/todos/1](http://localhost:8080/todos/1)

Req#5: Update

curl -d '{"id":1,"txt":"my updated todo","completed":true}' -H "Content-Type: application/json" -X PUT [http://localhost:8080/todos/1](http://localhost:8080/todos/1)

If you run the cURL commands in order, you won’t get anything from the #1 and #2 because the arraylist is empty. Also, you will get an error if you run #5 because the Todo will have been deleted. Therefore, use the commands as a reference, and try them out in a sensible order to confirm the application is behaving as you would expect. E.g.

Check Empty (#1), Add one (#3), Check not empty (#1), Get 1 (#2), Update todo (#5), Check is update (#2), Delete Todo (#4), Check Empty (#1), Check can’t now retrieve single todo (#2).

And that should give you everything you need to manually test. However, that’s not great for modifying your application and checking you haven’t broken anything. That’s a lot of manual work to repeat every change you make. So, lets make some automated tests to do something similar.

Unit Testing our Service using RestEasy

By default (if you used the maven archetype), Quarkus will have generated a Test class, configured with RestEasy. Go to src/test/kotlin/codemwnci/TodoServiceTest.kt, and take a look at the code.

Delete the default test that was generated, and replace it with

@Test
fun testTodos() {
   given().`when`().get("/todos").then().statusCode(200).body(containsString("[]"))
}

And make sure you add the following imports

import org.hamcrest.CoreMatchers.containsString;
import org.hamcrest.core.IsNot.not;

This test will test that if we call the URL /todos, which is our getAll, we return an empty JSON list.

One thing to note as we build up our test suite, is that we cannot guarantee the order that the tests are run. When we move on to the database section of this tutorial, I will show how we can clean-down the in-memory database before each test run, but for now, we will have to run all our tests inside a single @Test function.

Update the function to now look like the following.

@Test
fun testTodos() {
   // Check Empty (#1)
   given().`when`().get("/todos").then().statusCode(200).body(containsString("[]"))
   // Add one (#3)
   given().body("test todo").`when`().post("/todos").then().statusCode(200).body(containsString("test todo"), containsString("""{"id":1,"txt":"test todo","completed":false}"""))
   // Check not empty (#1)
   given().`when`().get("/todos").then().statusCode(200).body(not(containsString("[]")))
   // Get 1 (#2)
   given().`when`().get("/todos/1").then().statusCode(200).body(containsString("test todo"))
   // Update todo (#5)
   given().contentType("application/json").body("""{"id":1,"txt":"real todo","completed":true}""").`when`().put("/todos/1").then().statusCode(200).body(not(containsString("test")), containsString("real"), containsString("true"))
   // Check is updated (#2)
   given().`when`().get("/todos/1").then().statusCode(200).body(containsString("real todo"))
   // Delete Todo (#4)
   given().`when`().delete("/todos/1").then().statusCode(200).body(containsString("true"))
   // Check Empty (#1)
   given().`when`().get("/todos").then().statusCode(200).body(containsString("[]"))
   // Check can’t now retrieve single todo (#2)
   given().`when`().get("/todos/1").then().statusCode(204)
}

The tests should test the same order of manual tests that we carried out in manual testing section. I am not going to go into this in detail, as I hope the tests themselves should be self explanatory. One note on status code 204 means No Content, which is what is expected if nothing (null) is returned from our GET request.

To run the tests, you can use mvn test. The tests will also run when mvn package is executed.

Finally

And that is it. Part 1 complete. A simple REST service, written in Kotlin using the Quarkus framework.

In part 2, I will add a database and update the code to use the database instead of the ArrayList. This will include adding an in memory database for the unit testing.

In part 3, I will add a front end application to make use of the service, instead of the cURL.