API REST com Kotlin e Kovert

API REST com Kotlin e Kovert

1. Introdução

Kovert is a REST API framework that is strongly opinionated, and thus very easy to get started with. Ele aproveita o poder deVert.x, mas torna significativamente mais fácil desenvolver aplicativos de forma consistente.

É possível escrevermos uma API Kovert do zero ou usar controladores Kovert em um aplicativo Vert.x existente. A biblioteca foi projetada para funcionar da maneira que desejamos.

2. Dependências do Maven

Kovert é uma biblioteca Kotlin padrão e está disponível emMaven Central:


    uy.kohesive.kovert
    kovert-vertx
    1.5.0

3. Iniciando um servidor Kovert

Kovert faz uso intenso deKodein para a fiação de nossa aplicação. Isso inclui a configuração de carregamento do Kovert e do Vert.x, além de todos os módulos necessários para que tudo funcione.

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

Vejamos um exemplo de arquivo de configuração para um servidor Kovert:

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

E então podemos iniciar uma instância do Kodein que cria e executa um servidor Kovert:

fun main(args: Array) {
    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)
        }
    }
}

Isso carregará nossa configuração e iniciará um servidor web executando como configurado.

Neste ponto, o servidor web não possui controladores.

4. Controladores Simples

One of the most powerful aspects of Kovert is the way that we get to write controllers. Não há necessidade de ditar ao sistema como atribuir solicitações HTTP ao código. Em vez disso, isso é orientado por convenção com base nos nomes dos métodos.

Nós escrevemos nossos controladores como classes simples com métodos nomeados em um padrão específico. Nossos métodos também precisam ser escritos como métodos de extensão na classeRoutingContext:

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

Isso define um único método de controlador que está vinculado aGET /string/:id. Um valor é fornecido como um parâmetro de caminho e este controlador o retorna como está.

Podemos então inseri-los no roteador Kovert no fechamentoinitControllers, e então todos eles estarão disponíveis no servidor em execução:

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

Isso monta os métodos em nosso controlador em/api - então nosso métodogetStringById() está realmente disponível em/api/string/:id. Não há limite para o número de controladores que podem ser montados no mesmo caminho, desde que nenhuma das URLs geradas entre em conflito.

4.1. Convenções de nomenclatura de métodos

As regras de como os nomes dos métodos são usados ​​para gerar URLs estão bem documentadas pelo aplicativo Kovert.

Em resumo:

  • A primeira palavra é usada como o nome do método HTTP - obter, postar, colocar, excluir etc. Aliases para esses também são possíveis, portanto, "remover" pode ser usado em vez de "excluir", por exemplo.

  • As palavras “By” e “In” são usadas para indicar que a próxima palavra é um parâmetro de caminho. Por exemplo,ById torna-se/:id.

  • A palavra "Com" é usada para indicar que a próxima palavra é um parâmetro do caminho e uma parte do caminho. Por exemplo,WithId torna-se/id/:id.

Todas as outras palavras são usadas como segmentos de caminho, separados em palavras individuais, cada um sendo um caminho diferente. Se precisarmos alterar isso, podemos usar sublinhados para separar as palavras, em vez de permitir que o Kovert resolva isso automaticamente.

Por exemplo:

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

Observe que, ao usar sublinhados para separar os segmentos do caminho, todos os segmentos devem começar com letras minúsculas. Isso inclui as palavras especiais "Por", "Em" e "Com".

Caso contrário, o Kovert os tratará como segmentos de caminho.

Por exemplo:

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. Respostas JSON

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"
    )
}

Isso define um único controlador para lidar com/person/:id. Se então solicitarmos/person/abc,, obteremos uma resposta JSON de:

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

Kovert uses Jackson to convert our responses, so we can use all of [.hardreadability] # as anotações com suporte para gerenciar isso, se necessário: #

data class Person(
    @JsonProperty("_id")
    val id: String,
    val name: String,
    val job: String
)

Agora, isso retornará o seguinte:

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

4.3. Respostas de erro

Sometimes we need to return an error to the client indicating that we can’t continue. O HTTP tem vários erros que podemos retornar por diferentes razões eKovert has a simple mechanism for handling these.

Para acionar esse mecanismo, precisamos simplesmente lançar uma exceção apropriada do nosso método do controlador. Kovert define uma exceção para cada código de status HTTP suportado e automaticamente faz a coisa certa se estes forem lançados:

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

Sometimes we also need a bit more control over what happens, então Kovert define duas exceções adicionais que podemos usar -HttpErrorCodeeHttpErrorCodeWithBody.

Diferentemente dos mais genéricos, eles farão com que a exceção seja enviada também para os logs do servidor e podem nos permitir determinar programaticamente o código de status - incluindo aqueles que não são suportados pelo padrão - e o corpo da resposta:

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

Como sempre, podemos usar qualquer objeto rico no corpo, e isso será automaticamente transformado em JSON.

5. Vinculação de controlador avançado

Embora na maioria das vezes possamos nos dar bem com o suporte simples ao controlador já coberto, às vezes precisamos de um pouco mais de suporte para fazer nosso aplicativo fazer exatamente o que queremos. Kovert offers us the ability to be flexible around a lot of things, allowing us to build the application we want.

5.1. Parâmetros da string de consulta

Sometimes we also need to have additional parameters passed to our controllers that are not part of the request path. Obteremos todos os valores passados ​​como parâmetros de string de consulta simplesmente adicionando parâmetros adicionais ao nosso método:

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

We also can specify default values for these parameters, para que possam ser fornecidos opcionalmente na URL.

Por exemplo, o acima fará:

  • /truncatedString/abc ⇒ “a”

  • /truncatedString/abc?length=2 ⇒ “ab”

5.2. Organismos de solicitação JSON

Muitas vezes, também queremos enviar dados estruturados para o nosso servidor. Kovert will automatically handle this for us if the request body is JSONe o controlador possui um parâmetro apropriado de um tipo rico.

Por exemplo, podemos enviar um novoPerson para o nosso servidor:

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

Isso criará um novo manipulador paraPUT /person/:id que aceita um corpo de solicitação JSON em conformidade com o bean Person. Isso é automaticamente disponibilizado para uso, conforme necessário.

5.3. Aliases de verbos personalizados

Ocasionalmente, podemos querer personalizar a maneira como o Kovert corresponde aos nomes dos nossos métodos para solicitar URLs. Em particular, podemos não estar satisfeitos com o conjunto padrão de aliases de verbos HTTP disponíveis.

Kovert nos oferece uma maneira fácil de gerenciar isso usando a chamadaKovertConfig.addVerbAlias. Isso nos permite registrar qualquer palavra que desejar para qualquer método HTTP, incluindo a substituição de métodos existentes, se desejar:

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

Isso nos permitirá escrever um nome de método desubmitPerson()e mapeará paraPOST /person automaticamente.

5.4. Métodos de anotação

Às vezes, precisamos ir ainda mais longe ao personalizar nossos métodos de controlador. Nesses casos,Kovert provides annotations that we can use on our methods to have complete control over the mappings.

No nível dos métodos individuais, podemos especificar o verbo HTTP exato e a URL correspondentes, usando as anotações@Verbe@Location. Por exemplo, o seguinte responderá a "GET / ping /: id":

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

Como alternativa, podemos substituir os apelidos de verbo para todos os métodos em uma única classe, em vez de para os métodos em todo o aplicativo, usando os métodos@VerbAliase@VerbAliases. Por exemplo, o seguinte responderá a "GET / string /: id":

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

6. Respostas assíncronas

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

Isso é bom para casos simples, mas como Vert.x executa um único thread de E / S, isso pode causar problemas se algum de nossos métodos de controlador precisar esperar para realizar algumas ações - por exemplo, se estivermos chamando um banco de dados não queremos bloquear todo o restante do aplicativo.

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

Tudo o que precisamos fazer é retornar umPromise<Result, Exception> - ondeResult é o tipo de retorno do manipulador - e obteremos o processamento assíncrono:

fun RoutingContext.getPersonById(id: String): Promise {
    task {
        return personService.getById(id) ?: throw HttpErrorNotFound()
    }
}

Isso iniciará um thread de segundo plano no qual podemos chamarpersonService para carregar os detalhes da pessoa que desejamos. Se encontrarmos um, retornamos como está e o Kovert o converterá em JSON para nós.

Se não encontrarmos um, lançamos umHttpErrorNotFound que faz com que um HTTP 404 seja retornado.

7. Contextos de roteamento

Até agora, nós escrevemos todos os nossos controladores usando oRoutingContext padrão como base. Isso funciona, mas não é a única opção.

Também podemos usar qualquer classe personalizada nossa, desde que tenha um construtor de parâmetro único que recebaRoutingContext. . Essa classe é o contexto para o método do controlador - ou seja, o valor dethis - e pode fazer qualquer coisa necessária para a chamada:

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 - verificando se o cabeçalho “Authorization” possui o valor “Secure”. Isso pode nos permitir abstrair ainda mais detalhes HTTP, para que nossas classes de controladores lidem apenas com chamadas de métodos simples, e a implementação delas não é uma preocupação.

Nesse caso, a maneira como determinamos que a solicitação foi protegida é pelo uso de cabeçalhos HTTP. Podemos usar parâmetros de string de consulta ou valores de sessão com a mesma facilidade, e o código do controlador não se importa.

Cada método de controlador único declara individualmente qual classe de contexto de roteamento deseja usar. Todos podem ser iguais ou cada um pode ser diferente, e Kovert fará a coisa certa.

8. Conclusão

Neste artigo, fornecemos uma introdução ao Kovert para escrever APIs REST simples em Kotlin.

Há muito mais do que podemos alcançar usando Kovert do que mostrado aqui. Felizmente, isso deve nos iniciar na jornada para APIs REST simples.

E, como sempre, veja os exemplos de toda essa funcionalidadeover on GitHub.