Quantcast
Channel: Baeldung
Viewing all articles
Browse latest Browse all 4535

REST API With Kotlin and Kovert

$
0
0

1. Introduction

Kovert is a REST API framework that is strongly opinionated, and thus very easy to get started with. It leverages the power of Vert.x but makes it significantly easier to develop applications consistently.

It’s possible for us to write a Kovert API from scratch, or to use Kovert controllers in an existing Vert.x application. The library is designed to work with however we wish to use it.

2. Maven Dependencies

Kovert is a standard Kotlin library and is available on Maven Central:

<dependency>
    <groupId>uy.kohesive.kovert</groupId>
    <artifactId>kovert-vertx</artifactId>
    <version>1.5.0</version>
</dependency>

3. Starting a Kovert Server

Kovert makes heavy use of Kodein for wiring our application. This includes loading configuration for Kovert and Vert.x as well as all of the required modules to make everything work.

We can start a simple Kovert server in a relatively small amount of code.

Let’s look at a sample configuration file for a Kovert server:

{
   kovert: {
       vertx: {
           clustered: false
       }
       server: {
           listeners: [
               {
                    host: "0.0.0.0"
                    port: "8000"
               }
           ]
       }
   }
}

And then we can start up a Kodein instance that builds and runs a Kovert server:

fun main(args: Array<String>) {
    NoopServer.start()
}

class NoopServer {
    companion object {
        private val LOG: Logger = LoggerFactory.getLogger(NoopServer::class.java)
    }

    fun start() {
        Kodein.global.addImport(Kodein.Module {
            val config =ClassResourceConfig("/kovert.conf", NoopServer::class.java)
            importConfig(loadConfig(config, ReferenceConfig())) {
                import("kovert.vertx", KodeinKovertVertx.configModule)
                import("kovert.server", KovertVerticleModule.configModule)
            }

            import(KodeinVertx.moduleWithLoggingToSlf4j)
            import(KodeinKovertVertx.module)
            import(KovertVerticleModule.module)
        })

        val initControllers = fun Router.() { }

        KovertVertx.start() bind { vertx ->
            KovertVerticle.deploy(vertx, routerInit = initControllers)
        } success { deploymentId ->
            LOG.warn("Deployment complete.")
        } fail { error ->
            LOG.error("Deployment failed!", error)
        }
    }
}

This will load our configuration and then start a web server running as configured.

At this point, the web server has no controllers.

4. Simple Controllers

One of the most powerful aspects of Kovert is the way that we get to write controllers. There is no need for us to dictate to the system how to assign HTTP requests to code. Instead, this is driven by convention based on method names.

We write our controllers as simple classes with methods named in a specific pattern. Our methods also need to be written as extension methods on the RoutingContext class:

class SimpleController {
    fun RoutingContext.getStringById(id: String) = id
}

This defines a single controller method that is bound to GET /string/:id. A value is provided as a path parameter, and this controller returns it as-is.

We can then insert them into the Kovert router in the initControllers closure, and then they will all be available from the running server:

val initControllers = fun Router.() {
    bindController(SimpleController(), "api")
}

This mounts the methods in our controller under /api – so our getStringById() method is actually available on /api/string/:id. There’s no limit on the number of controllers that can be mounted under the same path, as long as none of the generated URLs clash.

4.1. Method Naming Conventions

The rules for how method names are used to generate URLs are all well documented by the Kovert application.

In brief though:

  • The first word is used as the HTTP Method name – get, post, put, delete, etc. Aliases for these are also possible, so “remove” can be used instead of “delete” for example.
  • The words “By” and “In” are used to indicate that the next word is a path parameter. For example, ById becomes /:id.
  • The word “With” is used to indicate that the next word is both a path parameter and a part of the path. For example, WithId becomes /id/:id.

All other words are used as path segments, separated into individual words each being a different path. If we need to change this, we can use underscores to separate words instead of allowing Kovert to work it out automatically.

For example:

fun getSomethingSimple()       // GET /something/simple
fun get_something_elseSimple() // GET /something/elseSimple

Note that, when using underscores to separate the path segments, all segments should start with lowercase letters. This includes the special words “By”, “In” and “With”.

If not then Kovert will treat them as path segments instead.

For example:

fun getTruncatedStringById()    // GET /truncated/string/:id
fun get_TruncatedString_By_Id() // GET /TruncatedString/By/Id
fun get_truncatedString_by_id() // GET /truncatedString/:id
fun get_truncatedString_by_Id() // GET /truncatedString/:Id

4.2. JSON Responses

By default, Kovert will return JSON responses for any beans that are returned from our controllers:

data class Person(
    val id: String,
    val name: String,
    val job: String
)

class JsonController {
    fun RoutingContext.getPersonById(id: String) = Person(
        id = id,
        name = "Tony Stark",
        job = "Iron Man"
    )
}

This defines a single controller to handle /person/:id. If we then request /person/abc, we’ll get a JSON response of:

{
    "id": "abc",
    "name": "Tony Stark",
    "job": "Iron Man"
}
Kovert uses Jackson to convert our responses, so we can use all of the supported annotations to manage this if needed:
data class Person(
    @JsonProperty("_id")
    val id: String,
    val name: String,
    val job: String
)

This will now return the following instead:

{
    "_id": "abc",
    "name": "Tony Stark",
    "job": "Iron Man"
}

4.3. Error Responses

Sometimes we need to return an error to the client indicating that we can’t continue. HTTP has a lot of various errors that we can return for different reasons, and Kovert has a simple mechanism for handling these.

To trigger this mechanism, we simply need to throw an appropriate exception from our controller method. Kovert defines an exception for each supported HTTP status code and automatically does the right thing if these are thrown:

fun RoutingContext.getForbidden() {
    throw HttpErrorForbidden() // Returns an HTTP 403
}

Sometimes we also need a bit more control over what happens, so Kovert defines two additional exceptions we can use – HttpErrorCode and HttpErrorCodeWithBody.

Unlike the more generic ones, these will cause the exception to be output to the server logs as well and can allow us to programmatically determine the status code – including ones that are not supported by standard – and response body:

fun RoutingContext.getError() {
    throw HttpErrorCode("Something went wrong", 590)
}
fun RoutingContext.getErrorbody() {
    throw HttpErrorCodeWithBody("Something went wrong", 591, "Body here")
}

As always, we can use any rich object in the body, and this will be automatically transformed into JSON.

5. Advanced Controller Binding

While most of the time we can get by with the simple controller support already covered, sometimes we need a bit more support to make our application do exactly what we want. Kovert offers us the ability to be flexible around a lot of things, allowing us to build the application we want.

5.1. Query String Parameters

Sometimes we also need to have additional parameters passed to our controllers that are not part of the request path. We’ll get any values passed in as query string parameters by simply adding additional parameters to our method:

fun RoutingContext.get_truncatedString_by_id(id: String, length: Int = 1) = 
    id.subSequence(0, length)

We also can specify default values for these parameters, so that they can be optionally provided on the URL.

For example, the above will do:

  • /truncatedString/abc => “a”
  • /truncatedString/abc?length=2 => “ab”

5.2. JSON Request Bodies

Often we want to be able to send structured data to our server as well. Kovert will automatically handle this for us if the request body is JSON and the controller has an appropriate parameter of a rich type.

For example, we can send a new Person to our server:

fun RoutingContext.putPersonById(id: String, person: Person) = person

This will create a new handler for PUT /person/:id that accepts a JSON request body conforming to the Person bean. This is then automatically made available to us to use as needed.

5.3. Custom Verb Aliases

On occasion, we might want to be able to customize the way Kovert matches our method names to request URLs. In particular, we might not be happy with the default set of HTTP verb aliases that are available.

Kovert gives us an effortless way to manage this using the KovertConfig.addVerbAlias call. This allows us to register any words we like for any HTTP methods, including replacing existing ones if we wish:

KovertConfig.addVerbAlias("submit", HttpVerb.POST)

This will allow us to write a method name of submitPerson() and it will map on to POST /person automatically.

5.4. Annotating Methods

We might sometimes need to go even further with customizing our controller methods. In such cases, Kovert provides annotations that we can use on our methods to have complete control over the mappings.

At the level of the individual methods, we can specify the exact HTTP verb and URL that is matched, using the @Verb and @Location annotations. For example, the following will respond to “GET /ping/:id”:

@Verb(HttpVerb.GET)
@Location("/ping/:id")
fun RoutingContext.ping(id: String) = id

Alternatively, we can override the verb aliases for all of the methods in a single class, instead of for the methods in the entire application, by using the @VerbAlias and @VerbAliases methods. For example, the following will respond to “GET /string/:id”:

@VerbAlias("show", HttpVerb.GET)
class AnnotatedController {
    fun RoutingContext.showStringById(id: String) = id
}

6. Asynchronous Responses

Up until now, all of our controller methods have been synchronous.

This is fine for simple cases, but because Vert.x runs a single I/O thread, this can cause problems if any of our controller methods ever need to wait to perform some actions – for example, if we’re calling a database then we don’t want to block the entire rest of the application.

Kovert is designed to work along with the Kovenant library to support asynchronous processing.

All we need to do is return a Promise<Result, Exception> – where Result is the return type of the handler – and we get asynchronous processing:

fun RoutingContext.getPersonById(id: String): Promise<Person, Exception> {
    task {
        return personService.getById(id) ?: throw HttpErrorNotFound()
    }
}

This will start a background thread in which we can call the personService to load the Person details that we want. If we find one we return it as-is, and Kovert will convert it to JSON for us.

If we don’t find one, we throw an HttpErrorNotFound which causes an HTTP 404 to be returned instead.

7. Routing Contexts

So far, we’ve written all of our controllers using the default RoutingContext as a base. This works, but it’s not the only option.

We can also use any custom class of our own as long as it has a single-parameter constructor that takes a RoutingContextThis class is then the context for the controller method – i.e. the value of this – and can do anything necessary for the call:

class SecuredContext(private val routingContext: RoutingContext) {
    val authenticated = routingContext.request().getHeader("Authorization") == "Secure"
}

class SecuredController {
    fun SecuredContext.getSecured() = this.authenticated
}

This provides a Context that allows us to determine if the call was secured or not — by checking if the “Authorization” header has the value “Secure”. This can then allow us to abstract away even more HTTP details so that our controller classes deal only with simple method calls, and the implementation of them is not a concern.

In this case, the way that we determine that the request was secured is by the use of HTTP headers. We can just as easily use query string parameters or session values, and the controller code doesn’t care.

Every single controller method individually declares which routing context class it wants to use. They could all be the same, or each one could be different, and Kovert will do the correct thing.

8. Conclusion

In this article, we’ve given an introduction to Kovert for writing simple REST APIs in Kotlin.

There’s a lot more than we can achieve using Kovert than shown here. Hopefully, this should get us started on the journey to simple REST APIs.

And, as always, check out the examples of all this functionality over on GitHub.


Viewing all articles
Browse latest Browse all 4535

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>