Server-side validation with Ktor
This tutorial provides a sampling of how Akkurate helps you write server-side validation with Ktor. We're going to create an HTTP API to manage the books contained within a library; its role is to ensure each book has a valid and unique ISBN, as well as a valid title.
Setting up the project
You can download a generated Ktor project by following this link. Then click and, once the project is downloaded, open it in IntelliJ.
Defining and persisting the data model
A book is composed of an ISBN and a title. ISBN stands for International Standard Book Number, a unique identifier assigned to each book.
To represent the book in the application, we will use the following data class:
The @Serializable
annotation is necessary because this class will be used to transmit JSON data over HTTP.
To store the Book
class, we will use Exposed and follow what's recommended in Database persistence with Exposed; allowing us to easily query our database and keep this tutorial as simple as possible.
First, we need to define our database schema to store all the properties of our books:
We're making the isbn
the primary key, since it's already a unique identifier. It's also a char(13)
type instead of a varchar
because an ISBN is always composed of 13 characters.
Next, we have to create our DAO, which will provide two essential methods:
create()
to store a new book in the database,list()
to retrieve all the books stored in our database.
Finally, we need to instantiate our DAO with a database connection when the application starts up. Open the Databases.kt file, create a top level variable lateinit var bookDao: BookDao
, and define it inside the configureDatabases
function:
Handling the requests
We will need two routes for our HTTP API; POST /books
to register a new book to the database, and GET /books
to list all the books in the database.
Open the Routing.kt file and copy the following code in the configureRouting
function:
The POST /books
route deserializes the payload, stores it in the database, and returns a 201 HTTP status code. The GET /books
route fetches all the books and serializes them into the response.
What can we improve?
Our API is done, you can run it either with IntelliJ or with the ./gradlew run
command.
Create a new book with isbn=123
and title
being empty:
Now list the books:
We can see two issues in this response; the ISBN is now filled up to the 13 required characters, and we shouldn't allow empty titles.
Try to run the first query again:
Now we have an internal error because we're trying to insert a second book with the same ISBN as the first one. This is impossible due to the ISBN being the primary key of the table.
Finally, try to create a book with a title over 50 characters:
Once again, we see another internal error, because our title is composed of 64 characters meanwhile our database column can contain a maximum of 50 characters.
Validating the requests
All these issues can be fixed by validating the requests. We will use Akkurate coupled to Ktor request validation.
Enhancing the DAO
Before writing any validation code, we need to add the following method to our BookDao
class to allow searching a book by its ISBN:
That way, we can check if a book exists by running bookDao.existsWithIsbn(isbn)
.
Writing validation constraints
First, we must install Akkurate dependencies:
Install in a single-platform project
Add KSP to your plugin list; make sure to use the appropriate version, depending on the Kotlin version you're using.
plugins { kotlin("jvm") version "2.0.20" id("com.google.devtools.ksp") version "2.0.20-1.0.25" }Add the dependencies and register the compiler plugin through KSP.
dependencies { implementation("dev.nesk.akkurate:akkurate-core:0.10.0") ksp("dev.nesk.akkurate:akkurate-ksp-plugin:0.10.0") }
Then mark the Book
class with the @Validate
annotation:
Just like in the Getting Started guide, we create a Validator
instance and add our constraints to it:
There are multiple things to explain here:
We use a suspendable validator with a context. Those allow our validator to call the
BookDao.existsWithIsbn
method, to ensure a book isn't already registered in our database.The call to
existsWithIsbn
is done within an inline constraint and only if the ISBN is valid, to avoid a useless query to the database.We ensure the title is not blank but also that it isn't longer than 50 characters, otherwise the database will reject it with an exception.
Wiring to Ktor validation
Akkurate runs the validation and returns a result, but it needs to provide the latter to Ktor in order to generate a response. This requires the Request Validation plugin:
This plugin allows configuring a validation function for a specific class; it will be executed on each deserialization. A validation result must be returned, we can generate it from Akkurate's own result:
Notice how we execute our validateBook
function with the provided book, then we map the result to Ktor's result.
We also have to call our configureValidation
function on application start, this is done in the Application.kt file:
When the validation fails, the plugin throws a RequestValidationException
. To handle this exception and return a proper response, we use the Status Pages plugin.
Open the Routing.kt file, navigate to the configureRouting
function, then the install(StatusPages) {}
lambda, and add the following code:
When the RequestValidationException
is thrown, the Status Page plugin catches it and returns a response with a 422 HTTP status code, along with a JSON array of validation messages.
Conformance checking
It's time to test our API once again.
Create a new book with invalid values:
This time, our validation took care of rejecting the request.
Now try to create a book with valid values:
The request is considered valid and we received a 201 HTTP status code.
What if we try to create the same book a second time?
As expected, we can't register the same ISBN twice.
Our API is now fully validated, which means security is improved, and the users can understand why the request failed.