Skip to the content.

Building RESTful APIs with Eclipse Vertx and Kotlin

As mentioned in the former posts, Eclipse Vertx expands its API to different languages such as Kotlin, Groovy via official bindings, and even Node/Typescript and PHP via community supports.

In this post, we will reimplement the former RESTful APIs with Kotlin language.

Open your browser and navigate to Eclipse Vertx Starter, and generate the project skeleton. Do not forget to select Kotlin in the language field.

For the existing project, add the following dependency into the pom.xml file.

<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-lang-kotlin</artifactId>
</dependency>

Configure kotlin-compiler-plugin to compile the Kotlin source codes.

<plugin>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-maven-plugin</artifactId>
    <version>${kotlin.version}</version>
    <configuration>
        <jvmTarget>16</jvmTarget>
    </configuration>
    <executions>
        <execution>
            <id>compile</id>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>
        <execution>
            <id>test-compile</id>
            <goals>
                <goal>test-compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

We will use the same file structure and migrate the original project(written in Java) to Kotlin.

Firstly let’s have a look at the entry class - MainVerticle.

class MainVerticle : AbstractVerticle() {
    companion object {
        private val LOGGER = Logger.getLogger(MainVerticle::class.java.name)

        /**
         * Configure logging from logging.properties file.
         * When using custom JUL logging properties, named it to vertx-default-jul-logging.properties
         * or set java.util.logging.config.file system property to locate the properties file.
         */
        @Throws(IOException::class)
        private fun setupLogging() {
            MainVerticle::class.java.getResourceAsStream("/logging.properties")
                .use { f -> LogManager.getLogManager().readConfiguration(f) }
        }

        init {
            LOGGER.info("Customizing the built-in jackson ObjectMapper...")
            val objectMapper = DatabindCodec.mapper()
            objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            objectMapper.disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
            objectMapper.disable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
            val module = JavaTimeModule()
            objectMapper.registerModule(module)
        }
    }

    @Throws(Exception::class)
    override fun start(startPromise: Promise<Void>) {
        LOGGER.log(Level.INFO, "Starting HTTP server...")
        //setupLogging();

        //Create a PgPool instance
        val pgPool = pgPool()

        //Creating PostRepository
        val postRepository = PostRepository(pgPool)

        //Creating PostHandler
        val postHandlers = PostsHandler(postRepository)

        // Initializing the sample data
        val initializer = DataInitializer(pgPool)
        initializer.run()

        // Configure routes
        val router = routes(postHandlers)

        // Create the HTTP server
        vertx.createHttpServer() // Handle every request using the router
            .requestHandler(router) // Start listening
            .listen(8888) // Print the port
            .onSuccess {
                startPromise.complete()
                println("HTTP server started on port " + it.actualPort())
            }
            .onFailure {
                startPromise.fail(it)
                println("Failed to start HTTP server:" + it.message)
            }
    }

    //create routes
    private fun routes(handlers: PostsHandler): Router {

        // Create a Router
        val router = Router.router(vertx)
        // register BodyHandler globally.
        //router.route().handler(BodyHandler.create());
        router.get("/posts")
            .produces("application/json")
            .handler { handlers.all(it) }

        router.post("/posts")
            .consumes("application/json")
            .handler(BodyHandler.create())
            .handler { handlers.save(it) }

        router.get("/posts/:id")
            .produces("application/json")
            .handler { handlers.getById(it) }
            .failureHandler {
                val error = it.failure()
                if (error is PostNotFoundException) {
                    it.response().setStatusCode(404).end(error.message)
                }
            }

        router.put("/posts/:id")
            .consumes("application/json")
            .handler(BodyHandler.create())
            .handler { handlers.update(it) }

        router.delete("/posts/:id")
            .handler { handlers.delete(it) }

        router.get("/hello").handler { it.response().end("Hello from my route") }

        return router
    }

    private fun pgPool(): PgPool {
        val connectOptions = PgConnectOptions()
            .setPort(5432)
            .setHost("localhost")
            .setDatabase("blogdb")
            .setUser("user")
            .setPassword("password")

        // Pool Options
        val poolOptions = PoolOptions().setMaxSize(5)

        // Create the pool from the data object
        return PgPool.pool(vertx, connectOptions, poolOptions)
    }
}

In this class we move the original static block to a companion object.

In the router function, it assembles request handlers in routes. Let’s have a look at the PostsHandlers class.

class PostsHandler(val posts: PostRepository) {
    fun all(rc: RoutingContext) {
//        var params = rc.queryParams();
//        var q = params.get("q");
//        var limit = params.get("limit") == null ? 10 : Integer.parseInt(params.get("q"));
//        var offset = params.get("offset") == null ? 0 : Integer.parseInt(params.get("offset"));
//        LOGGER.log(Level.INFO, " find by keyword: q={0}, limit={1}, offset={2}", new Object[]{q, limit, offset});
        posts.findAll()
            .onSuccess {
                rc.response().end(Json.encode(it))
            }

    }

    fun getById(rc: RoutingContext) {
        val params = rc.pathParams()
        val id = params["id"]
        posts.findById(UUID.fromString(id))
            .onSuccess { rc.response().end(Json.encode(it)) }
            .onFailure { rc.fail(404, it) }
    }

    fun save(rc: RoutingContext) {
        //rc.getBodyAsJson().mapTo(PostForm.class)
        val body = rc.bodyAsJson
        LOGGER.log(Level.INFO, "request body: {0}", body)
        val (title, content) = body.mapTo(CreatePostCommand::class.java)
        posts.save(Post(title = title, content = content))
            .onSuccess { savedId: UUID ->
                rc.response()
                    .putHeader("Location", "/posts/$savedId")
                    .setStatusCode(201)
                    .end()
            }
    }

    fun update(rc: RoutingContext) {
        val params = rc.pathParams()
        val id = params["id"]
        val body = rc.bodyAsJson
        LOGGER.log(Level.INFO, "\npath param id: {0}\nrequest body: {1}", arrayOf(id, body))
        var (title, content) = body.mapTo(CreatePostCommand::class.java)
        posts.findById(UUID.fromString(id))
            .flatMap { post: Post ->
                post.apply {
                    title = title
                    content = content
                }
                posts.update(post)
            }
            .onSuccess { rc.response().setStatusCode(204).end() }
            .onFailure { rc.fail(it) }
    }

    fun delete(rc: RoutingContext) {
        val params = rc.pathParams()
        val id = params["id"]
        val uuid = UUID.fromString(id)
        posts.findById(uuid)
            .flatMap { posts.deleteById(uuid) }
            .onSuccess { rc.response().setStatusCode(204).end() }
            .onFailure { rc.fail(404, it) }
    }

    companion object {
        private val LOGGER = Logger.getLogger(PostsHandler::class.java.simpleName)
    }
}

Let’s move to the PostRepository class.

class PostRepository(private val client: PgPool) {

    fun findAll() = client.query("SELECT * FROM posts ORDER BY id ASC")
        .execute()
        .map { rs: RowSet<Row?> ->
            StreamSupport.stream(rs.spliterator(), false)
                .map { mapFun(it!!) }
                .toList()
        }


    fun findById(id: UUID) = client.preparedQuery("SELECT * FROM posts WHERE id=$1")
        .execute(Tuple.of(id))
        .map { it.iterator() }
        .map {
            if (it.hasNext()) mapFun(it.next());
            throw PostNotFoundException(id)
        }


    fun save(data: Post) = client.preparedQuery("INSERT INTO posts(title, content) VALUES ($1, $2) RETURNING (id)")
        .execute(Tuple.of(data.title, data.content))
        .map { it.iterator().next().getUUID("id") }


    fun saveAll(data: List<Post>): Future<Int> {
        val tuples = data.map { Tuple.of(it.title, it.content) }

        return client.preparedQuery("INSERT INTO posts (title, content) VALUES ($1, $2)")
            .executeBatch(tuples)
            .map { it.rowCount() }
    }

    fun update(data: Post) = client.preparedQuery("UPDATE posts SET title=$1, content=$2 WHERE id=$3")
        .execute(Tuple.of(data.title, data.content, data.id))
        .map { it.rowCount() }


    fun deleteAll() = client.query("DELETE FROM posts").execute()
        .map { it.rowCount() }


    fun deleteById(id: UUID) = client.preparedQuery("DELETE FROM posts WHERE id=$1").execute(Tuple.of(id))
        .map { it.rowCount() }

    companion object {
        private val LOGGER = Logger.getLogger(PostRepository::class.java.name)
        val mapFun: (Row) -> Post = { row: Row ->
            Post(
                row.getUUID("id"),
                row.getString("title"),
                row.getString("content"),
                row.getLocalDateTime("created_at")
            )
        }

    }
}

The POJO classes are converted to Kotlin data classes.

//Models.kt
data class Post(
    var id: UUID? = null,
    var title: String,
    var content: String,
    var createdAt: LocalDateTime? = LocalDateTime.now()
)

data class CreatePostCommand(
    val title: String,
    val content: String
)

The DataIntializer is still used to insert some sample data at the application startup.

class DataInitializer(private val client: PgPool) {

    fun run() {
        LOGGER.info("Data initialization is starting...")
        val first = Tuple.of("Hello Quarkus", "My first post of Quarkus")
        val second = Tuple.of("Hello Again, Quarkus", "My second post of Quarkus")
        client
            .withTransaction { conn: SqlConnection ->
                conn.query("DELETE FROM posts").execute()
                    .flatMap {
                        conn.preparedQuery("INSERT INTO posts (title, content) VALUES ($1, $2)")
                            .executeBatch(listOf(first, second))
                    }
                    .flatMap {
                        conn.query("SELECT * FROM posts").execute()
                    }
            }
            .onSuccess { data: RowSet<Row?> ->
                StreamSupport.stream(data.spliterator(), true)
                    .forEach {
                        LOGGER.log(Level.INFO, "saved data:{0}", it!!.toJson())
                    }
            }
            .onComplete {
                //client.close(); will block the application.
                LOGGER.info("Data initialization is done...")
            }
            .onFailure { LOGGER.warning("Data initialization is failed:" + it.message) }
    }

    companion object {
        private val LOGGER = Logger.getLogger(DataInitializer::class.java.name)
    }
}

Run the application via maven command.

mvn clean compile exec:java

Additionally, Vertx Kotlin bindings provides a Json DSL extension to simplify the JSON encoding.

 it.response()
     .setStatusCode(404)
     .end(
         json {// an example using JSON DSL
             obj(
                 "message" to "${it.failure().message}",
                 "code" to "not_found"
             )
         }.toString()
     )

Get the source codes from my Github.