idanirf / tenistas-rest-ktor-2022-2023

Api REST para la gestión de tenistas usando Kotlin y Ktor. Programación de Servicios y Procesos. 2º DAM.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Tenistas REST Ktor

Api REST de Tenistas con Ktor para Programación de Servicios y Procesos de 2º de DAM. Curso 2022/2023

Kotlin LICENSE GitHub

imagen

Descripción

El siguiente proyecto es una API REST de Tenistas con Ktor para Programación de Servicios y Procesos de 2º de DAM. Curso 2022/2023. En ella se pretende crear un servicio completo para la gestión de tenistas, raquetas y representantes de marcas de raquetas.

El objetivo es que el alumnado aprenda a crear un servicio REST con Ktor, con las operaciones CRUD, securizar el servicio con JWT y usar un cliente para consumir el servicio. Se pretende que el servicio completo sea asíncrono y reactivo en lo máximo posible agilizando el servicio mediante una caché.

Además que permita escuchar cambios en tiempo real usando websocket

Se realizará inyección de dependencias y un sistema de logging.

Tendrá una página web de presentación como devolución de recursos estáticos.

Este proyecto tiene a su "gemelo" implementando en Ktor: tenistas-rest-springboot-2022-2023

Advertencia

Esta API REST no está pensada para ser usada en producción. Es un proyecto de aprendizaje y por tanto algunas cosas no se profundizan y otras están pensadas para poder realizarlas en clase de una manera más simple con el objetivo que el alumnado pueda entenderlas mejor. No se trata de montar la mejor arquitectura o el mejor servicio, sino de aprender a crear un servicio REST en el tiempo exigido por el calendario escolar.

Este proyecto está en constante evolución y se irán añadiendo nuevas funcionalidades y mejoras para el alumnado. De la misma manera se irá completando la documentación asociada.

Si quieres colaborar, puedes hacerlo contactando conmigo.

Tecnologías

  • Servidor Web: Ktor - Framework para crear servicios web en Kotlin asíncronos y multiplataforma.
  • Autenticación: JWT - JSON Web Token para la autenticación y autorización.
  • Encriptado: Bcrypt - Algoritmo de hash para encriptar contraseñas.
  • Proveedor de dependencias: Koin - Framework para la inyección de dependencias.
  • Asincronía: Coroutines - Librería de Kotlin para la programación asíncrona.
  • Logger: Kotlin Logging - Framework para la gestión de logs.
  • Caché: Cache4k - Versión 100% Kotlin asíncrona y multiplataforma de Caffeine.
  • Base de datos: H2 - Base de datos relacional que te permite trabajar en memoria, fichero y servidor.
  • Librería base de Datos: Kotysa - Librería para la gestión de bases de datos en Kotlin que te permite operar reactivamente bajo R2DBC.
  • Notificaciones en tiempo real: Ktor WebSockets - Framework para la gestión de websockets.
  • Testing: JUnit 5 - Framework para la realización de tests unitarios, Mockk librería de Mocks para Kotlin, así como las propias herramientas de Ktor.
  • Cliente: Postman - Cliente para realizar peticiones HTTP.
  • Contenedor: Docker - Plataforma para la creación y gestión de contenedores.
  • Documentación: Dokka y Swagger - Herramienta para la generación de documentación y pruebas de API REST respectivamente mediante OpenAPI.

Dominio

Gestionar tenistas, raquetas y representantes de marcas de raquetas. Sabemos que:

  • Una raqueta tiene un representante y el representante es solo de una marca de raqueta (1-1). No puede haber raquetas sin representante y no puede haber representantes sin raquetas.
  • Un tenista solo puede o no tener contrato con una raqueta y una raqueta o modelo de raqueta puede ser usada por varios tenistas (1-N). Puede haber tenistas sin raqueta y puede haber raquetas sin tenistas.
  • Por otro lado tenemos usuarios con roles de administrador y usuarios que se pueden registrar, loguear consultar los datos y acceder a los datos de los usuarios (solo administradores).

Representante

Campo Tipo Descripción
id UUID Identificador único
nombre String Nombre del representante
email String Email del representante

Raqueta

Campo Tipo Descripción
id UUID Identificador único
marca String Marca de la raqueta
precio Double Precio de la raqueta
representante Representante Representante de la raqueta (no nulo)

Tenista

Campo Tipo Descripción
id UUID Identificador único
nombre String Nombre del tenista
ranking Int Ranking del tenista
fechaNacimiento LocalDate Fecha de nacimiento del tenista
añoProfesional Int Año en el que se convirtió en profesional
altura Double Altura del tenista
peso Double Peso del tenista
manoDominante String Mano dominante del tenista (DERECHA/IZQUIERDA)
tipoReves String Tipo de revés del tenista (UNA_MANO/DOS_MANOS)
puntos Int Puntos del tenista
pais String País del tenista
raquetaID UUID Identificador de la raqueta (puede ser nulo)

Usuario

Campo Tipo Descripción
id UUID Identificador único
nombre String Nombre del usuario
email String Email del usuario
username String Rol del usuario
password String Contraseña del usuario
avatar String Avatar del usuario
rol Rol Rol del usuario (ADMIN o USER)

Proyectos y documentación anteriores

Parte de los contenidos a desarrollar en este proyecto se han desarrollado en proyectos anteriores. En este caso:

Para la parte de reactividad te recomiendo leer: "Ya no sé programar si no es reactivo"

Arquitectura

Nos centraremos en la arquitectura de la API REST. Para ello, usaremos el patrón de diseño MVC (Modelo Vista Controlador) en capas.

img_1.png

img_2.png

Endpoints

Recuerda que puedes conectarte de forma segura:

Los endpoints que vamos a usar a nivel de api, parten de /api/ y puedes usarlos con tu cliente favorito. En este caso, usaremos Postman:

Representantes

Método Endpoint (/api) Auth Descripción Status Code (OK) Content
GET /representantes No Devuelve todos los representantes 200 JSON
GET /representantes?page=X&perPage=Y No Devuelve representantes paginados 200 JSON
GET /representantes/{id} No Devuelve un representante por su id 200 JSON
POST /representantes No Crea un nuevo representante 201 JSON
PUT /representantes/{id} No Actualiza un representante por su id 200 JSON
DELETE /representantes/{id} No Elimina un representante por su id 204 No Content
GET /representantes/find?nombre=X No Devuelve los representantes con nombre X 200 JSON
WS /updates/representantes No Websocket para notificaciones los cambios en los representantes en tiempo real --- JSON

Raquetas

Método Endpoint (/api) Auth Descripción Status Code (OK) Content
GET /raquetas No Devuelve todas las raquetas 200 JSON
GET /raquetas?page=X&perPage=Y No Devuelve raquetas paginadas 200 JSON
GET /raquetas/{id} No Devuelve una raqueta por su id 200 JSON
POST /raquetas No Crea una nueva raqueta 201 JSON
PUT /raquetas/{id} No Actualiza una raqueta por su id 200 JSON
DELETE /raquetas/{id} No Elimina una raqueta por su id 204 No Content
GET /raquetas/find?marca=X No Devuelve las raquetas con marca X 200 JSON
GET /raquetas/{id}/representante No Devuelve el representante de la raqueta dado su id 200 JSON
WS /updates/raquetas No Websocket para notificaciones los cambios en las raquetas en tiempo real --- JSON

Tenistas

Método Endpoint (/api) Auth Descripción Status Code (OK) Content
GET /tenistas No Devuelve todos los tenistas 200 JSON
GET /tenistas?page=X&perPage=Y No Devuelve tenistas paginados 200 JSON
GET /tenistas/{id} No Devuelve un tenista por su id 200 JSON
POST /tenistas No Crea un nuevo tenista 201 JSON
PUT /tenistas/{id} No Actualiza un tenista por su id 200 JSON
DELETE /tenistas/{id} No Elimina un tenista por su id 204 No Content
GET /tenistas/find?nombre=X No Devuelve los tenistas con nombre X 200 JSON
GET /tenistas/{id}/raqueta No Devuelve la raqueta del tenista dado su id 200 JSON
GET /tenistas/ranking/{ranking} No Devuelve el tenista con ranking X 200 JSON
WS /updates/tenistas No Websocket para notificaciones los cambios en los tenistas en tiempo real --- JSON

Usuarios

Método Endpoint (/api) Auth Descripción Status Code (OK) Content
POST /users/login No Login de un usuario, Token 200 JSON
POST /users/register No Registro de un usuario 201 JSON
GET /users/me JWT Datos del usuario del token 200 JSON
PUT /users/me JWT Actualiza datos del usuario: nombre, e-mail y username 200 JSON
PATCH /users/me JWT Actualiza avatar del usuario como multipart 200 JSON
GET /users/list JWT Devuelve todos los usuarios, si el token pertenece a un admin 200 JSON

Storage

Método Endpoint (/api) Auth Descripción Status Code (OK) Content
GET /storage/check NO Info del servicio 200 JSON
POST /storage No Envía un fichero como stream de bytes 201 JSON
GET /storage/{fileName} No Descarga un fichero por su nombre 200 JSON
DELETE /storage/{fileName} JWT Elimina un fichero por su nombre 204 No Content

Test

Método Endpoint (/api) Auth Descripción Status Code (OK) Content
GET /test?texto No Devuelve un JSON con datos de prueba, y el texto de query opcional 200 JSON
GET /test/{id} No Devuelve un JSON con datos de prueba por su id 200 JSON
POST /test No Crea un nuevo JSON con datos de prueba 201 JSON
PUT /test/{id} No Actualiza un JSON con datos de prueba por su id 200 JSON
PATCH /test/{id} No Actualiza un JSON con datos de prueba por su id 200 JSON
DELETE /test/{id} No Elimina un JSON con datos de prueba por su id 204 No Content

Ktor

Ktor es el framework para desarrollar servicios y clientes asincrónicos. Es 100% Kotlin y se ejecuta en usando Coroutines. Admite proyectos multiplataforma, lo que significa que puede usarlo para cualquier proyecto dirigido a JVM, Android, iOS, nativo o Javascript. En este proyecto aprovecharemos Ktor para crear un servicio web para consumir una API REST. Además, aplicaremos Ktor para devolver páginas web.

Ktor trabaja con un sistema de plugins que lo hacen muy flexible y fácil de configurar. Además, Ktor es un framework donde trabajamos con DSL (Domain Specific Language) que nos permite crear código de forma más sencilla y legible.

Además, permite adaptar su estructura en base a funciones de extensión.

img_3.png

Creando un proyecto

Podemos crear un proyecto Ktor usando el plugin IntelliJ, desde su web. Con estos asistentes podemos crear un proyecto Ktor con las opciones que queramos (plugins), destacamos el routing, el uso de json, etc.

Punto de Entrada

El servidor tiene su entrada y configuración en la clase Application. Esta lee la configuración en base al fichero de configuración y a partir de aquí se crea una instancia de la clase Application en base a la configuración de module().

Parametrizando la aplicación

Podemos parametrizar la aplicación usando el fichero de configuración. En este caso, usaremos el fichero de configuración .conf y puede ser en distintos formatos, como JSON, YAML o HOCON. En este caso, usaremos HOCON. En este fichero de configuración podemos definir distintas propiedades, como el puerto de escucha, el host, el tiempo de expiración del token JWT, o el modo Auto-Reload, etc. En este caso, usaremos el siguiente fichero de configuración:

ktor {
    ## Para el puerto
    deployment {
        port = 6969
        port = ${?PORT}
    }

    ## Para la clase principal
    application {
        modules = [ joseluisgs.es.ApplicationKt.module ]
    }

    ## Modo de desarrollo, se dispara cuando detecta cambios
    ## development = true
    deployment {
        ## Directorios a vigilar
        watch = [ classes, resources ]
    }

    ## Modo de ejecución
    environment = dev
    environment = ${?KTOR_ENV}
}

Usando Plugins

Ktor se puede extender y ampliar usando plugins. Estos plugins se "instalan" y configuran configuran según las necesidades. Los más recomendados para hacer una Api Rest son:

  • Routing: Para definir las rutas de la API
  • Serialization: Para serializar y deserializar objetos, por ejemplo en JSON
  • ContentNegotiation: Para definir el tipo de contenido que se va a usar en la API, por ejemplo JSON
fun Application.configureSerialization() {
    install(ContentNegotiation) {
        // Lo ponemos bonito :)
        json(Json {
            prettyPrint = true
            isLenient = true
        })
    }
}

Creando rutas

Las rutas se definen creando una función de extensión sobre Route. A su vez, usando DSL se definen las rutase en base a las petición HTTP sobre ella. Podemos responder a la petición usando call.respondText(), para texto; call.respondHTML(), para contenido HTML usando Kotlin HTML DSL; o call.respond() para devolver una respuesta en formato JSON o XML. finalmente asignamos esas rutas a la instancia de Application, es decir, dentro del método module(). Un ejemplo de ruta puede ser:

routing {
    // Entrada en la api
    get("/") {
        call.respondText("👋 Hola Kotlin REST Service con Kotlin-Ktor")
    }
}

Type-Safe Routing y Locations

Ktor te permite hacer Type-Safe Routing, es decir, que puedes definir una clase que represente una ruta y que tenga las operaciones a realizar.

También podemos crear rutas de manera tipada con Locations, pero esta siendo sustituida por Type-Safe Routing.

Serialización y Content Negotiation

Ktor soporta Content Negotiation, es decir, que puede aceptar peticiones y respuestas distintos tipos de contenido, como JSON, XML, HTML, etc. En este caso, usaremos JSON. Para ello, usaremos la librería Kotlinx Serialization

install(ContentNegotiation) {
    json(Json {
        prettyPrint = true
        isLenient = true
    })
}

Otros plugins

Cache Headers

Nos permite configurar los encabezados Cache-Control y Expires utilizados para el almacenamiento en caché de HTTP. Puede configurar el almacenamiento en caché de las siguientes maneras: globales, particulares a nivel de ruta o llamada, activando o desactivando esta opción para determinados tipos de contenidos.

Comprensión de contenido

Ktor proporciona la capacidad de comprimir contenido saliente usando diferentes algoritmos de compresión, incluidos gzip y deflate, y con ello, especificar las condiciones requeridas para comprimir datos (como un tipo de contenido o tamaño de respuesta) o incluso comprimir datos en función de parámetros de solicitud específicos.

CORS

Si se supone que su servidor debe manejar solicitudes de origen cruzado (CORS), debe instalar y configurar el complemento CORS Ktor. Este complemento le permite configurar hosts permitidos, métodos HTTP, encabezados establecidos por el cliente, etc.

Por defecto, el plugin de CORS permite los métodos GET, POST y HEAD

Lo ideal es que aprendas a configurarlo según tus necesidades, pero aquí tienes un ejemplo de configuración básica:

install(CORS) {
    // podemos permitir algún host específico
    anyHost() // cualquier host, quitar en produccion
    allowHost("client-host")
    allowHost("client-host:8081")
    allowHost("client-host", subDomains = listOf("en", "de", "es"))
    allowHost("client-host", schemes = listOf("http", "https"))

    // Podemos permitir contenido
    allowHeader(HttpHeaders.ContentType) // Permitimos el tipo de contenido
    allowHeader(HttpHeaders.Authorization) // Permitimos autorithachion

    // Si queremos permitir otros métodos
    allowMethod(HttpMethod.Options)
    allowMethod(HttpMethod.Put)
    allowMethod(HttpMethod.Patch)
    allowMethod(HttpMethod.Delete)
}

Responses

En Ktor podemos mandar distintos tipos de respuesta, así como distintos códigos de estado.

call.respondText("👋 Hola Kotlin REST Service con Kotlin-Ktor")
call.respond(HttpStatusCode.OK, "👋 Hola Kotlin REST Service con Kotlin-Ktor")
call.respond(HttpStatusCode.NotFound, "No encontrado")

Enviando datos serializados

Simplemente usa una data class y la función call.respond() para enviar datos serializados. En este caso, usaremos la librería Kotlinx Serialization

@Serializable
data class Customer(val id: Int, val firstName: String, val lastName: String)

get("/customer") {
    call.respond(Customer(1, "José Luis", "García Sánchez"))
}

Requests

En Ktor podemos recibir distintos tipos de peticiones.

Parámetros de ruta

Podemos obtener los parámetros del Path, con parameters, como en el siguiente ejemplo, siempre y cuando estén definidos en la ruta {param}:

get("/hello/{name}") {
    val name = call.parameters["name"]
    call.respondText("Hello $name!")
}

Parámetros de consulta

Podemos obtener los parámetros de la Query, con queryParameters, si tenemos por ejemplo la siguiente ruta: /products?price=asc&category=1:

get("/products") {
    val price = call.request.queryParameters["price"]
    val category = call.request.queryParameters["category"]
    call.respondText("Price: $price, Category: $category")
}

Peticiones datos serializados

Para recibir datos serializados, usa la función call.receive() y la data class que representa el tipo de datos que se espera recibir con la que casteamos el body de la petición. En este caso, usaremos la librería Kotlinx Serialization

@Serializable
data class Customer(val id: Int, val firstName: String, val lastName: String)

post("/customer") {
    val customer = call.receive<Customer>()
    call.respondText("Customer: $customer")
}

Peticiones con formularios

Ktor soporta peticiones con formularios, es decir, que podemos enviar datos de un formulario.

post("/signup") {
    val formParameters = call.receiveParameters()
    val username = formParameters["username"].toString()
    call.respondText("The '$username' account is created")
}

Peticiones multiparte

Ktor soporta peticiones multipartes, es decir, que podemos enviar ficheros, imágenes, etc.

post("/upload") {
    //  multipart data (suspending)
    val multipart = call.receiveMultipart()
    multipart.forEachPart { part ->
        val fileName = part.originalFileName as String
        var fileBytes = part.streamProvider().readBytes()
        File("uploads/$fileName").writeBytes(fileBytes)
        part.dispose()
    }
    call.respondText("$fileName is uploaded to 'uploads/$fileName'")
}

Subida de información

Ktor soporta subida de información, es decir, que podemos enviar ficheros, imágenes, etc. Podemos hacerlo con recieve o receiveChannel() (raw). Para el caso de ficheros se puede mandar así si sabemos cómo almacenarlos, si no podemos enviar ficheros usando el sistema de petición multipart.

post("/upload") {
    val file = File("uploads/ktor_logo.png")
    call.receiveChannel().copyAndClose(file.writeChannel())
    call.respondText("A file is uploaded")
}

Request validation

Ktor tiene una API de validación que nos permite validar los datos del body de una petición. En este caso lanzando RequestValidationException si no es correcto.

install(RequestValidation) {
    validate<Customer> { customer ->
        if (customer.id <= 0)
            ValidationResult.Invalid("A customer ID should be greater than 0")
        else ValidationResult.Valid
    }
}

WebSockets

Ktor soporta WebSockets para crear aplicaciones que hagan uso de ellos. Los WebSockets permiten crear aplicaciones que requieren transferencia de datos en tiempo real desde y hacia el servidor ya que que hace posible abrir una sesión de comunicación interactiva entre el navegador del usuario y un servidor. Con esta API, puede enviar mensajes a un servidor y recibir respuestas controladas por eventos sin tener que consultar al servidor para una respuesta.

webSocket("/echo") {
    send("Please enter your name")
    for (frame in incoming) {
        frame as? Frame.Text ?: continue
        val receivedText = frame.readText()
        if (receivedText.equals("bye", ignoreCase = true)) {
            close(CloseReason(CloseReason.Codes.NORMAL, "Client said BYE"))
        } else {
            send(Frame.Text("Hi, $receivedText!"))
        }
    }
}

SSL y Certificados

Aunque lo normal, es que nuestros servicios estén detrás de un Proxy Inverso, podemos configurar Ktor para que soporte SSL y certificados. Para ello, debemos añadir la librería de soporte para TSL, y configurar el puerto y el certificado en el fichero application.conf.

ktor {
    ## Para el puerto
    deployment {
        ## Si no se especifica el puerto, se usa el 8080, si solo queremos SSL quitar el puerto normal
        port = 6969
        port = ${?PORT}
        ## Para SSL, si es necesario poner el puerto
        sslPort = 6963
        sslPort = ${?SSL_PORT}
    }

    ## Para la clase principal
    application {
        modules = [ joseluisgs.es.ApplicationKt.module ]
    }

    ## Para SSL/TSL configuración del llavero y certificado
    security {
        ssl {
            keyStore = ./cert/server_keystore.p12
            keyAlias = serverKeyPair
            keyStorePassword = 1234567
            privateKeyPassword = 1234567
        }
    }
}

Autenticación y Autorización con JWT

Ktor tiene una API de autenticación que nos permite autenticar usuarios y autorizar peticiones. En este caso, usaremos JWT para la autenticación y autorización. Para ello, debemos añadir la librería de soporte para Ktor JWT y configurar sus opciones.

Gracias a ella podemos crear un interceptor (middleware) que se ejecutará antes de cada petición y que nos permitirá validar el token JWT y añadirlo a la petición para que podamos usarlo en el resto de la aplicación. En este caso, usaremos el token para añadir el usuario autenticado a la petición y poder usarlo en el resto de la aplicación.

// Instalamos el interceptor de autenticación
install(Authentication) {
    jwt("auth-jwt") {
        validate { credential ->
            if (credential.payload.getClaim("username").asString() != "") {
                JWTPrincipal(credential.payload)
            } else {
                null
            }
        }
    }
}

// Añadimos el interceptor a todas las rutas
routing {
    authenticate("auth-jwt") {
        get("/hello") {
            val principal = call.principal<JWTPrincipal>() // Leemos el token
            val username = principal!!.payload.getClaim("username").asString()
            val expiresAt = principal.expiresAt?.time?.minus(System.currentTimeMillis())
            call.respondText("Hello, $username! Token is expired at $expiresAt ms.")
        }
    }
}

Testing

Ktor tiene una API de testing que nos permite testear nuestras aplicaciones. Para ello, debemos añadir la librería de soporte para Ktor Test y configurar sus opciones.

Podemos usar la función testApplication para configurar una instancia configurada de nuestro servicio de prueba que se ejecuta localmente. Con ello podemos usar la instancia de cliente HTTP de Ktor dentro de una aplicación de prueba para realizar una solicitud a su servidor, recibir una respuesta y testear resultados.

fun registerUserTest() = testApplication {
    // Configuramos el entorno de test
    environment { config }

    val client = createClient {
        install(ContentNegotiation) {
            json()
        }
    }

    // Lanzamos la consulta
    val response = client.post("/api/users/register") {
        contentType(ContentType.Application.Json)
        setBody(userDto)
    }

    // Comprobamos que la respuesta y el contenido es correcto
    assertEquals(response.status, HttpStatusCode.Created)
    // Tambien podemos comprobar el contenido
    val res = json.decodeFromString<UserDto>(response.bodyAsText())
    assertAll(
        { assertEquals(res.nombre, userDto.nombre) },
        { assertEquals(res.email, userDto.email) },
        { assertEquals(res.username, userDto.username) },
        { assertEquals(res.avatar, userDto.avatar) },
        { assertEquals(res.role, userDto.role) },
    )
}

Despliegue

Podemos distribuir nuestra app de distintas maneras

JAR

Podemos crear un JAR con nuestra aplicación y ejecutarla con el comando java -jar. Para ello, debemos añadir la librería de soporte para Ktor JAR y configurar sus opciones en Gradle.

  • buildFatJar: construye un JAR combinado de un proyecto y dependencias, como *-all.jar en el directorio build/libs cuando se complete esta compilación.
  • runFatJar: construye un JAR del proyecto y lo ejecuta.

Aplicación

Podemos crear una aplicación y ejecutarla gracias a Gradle. Para ello, debemos añadir la librería de soporte para Ktor Application y configurar sus opciones.

Esta opción nos proporciona varias formas de empaquetar la aplicación, por ejemplo, la tarea installDist instala la aplicación con todas las dependencias de tiempo de ejecución y los scripts de inicio. Para crear archivos de distribución completos.

Docker

Ktor tiene una API de Docker que nos permite crear una imagen de Docker con nuestra aplicación.

  • buildImage: construye la imagen de Docker de un proyecto en un tarball. Esta tarea genera un archivo jib-image.tar en el directorio de compilación. Puede cargar esta imagen en un demonio de Docker con el comando de carga de Docker: docker load < build/jib-image.tar
  • publishImageToLocalRegistry: compila y publica la imagen de Docker de un proyecto en un registro local.
  • runDocker: crea la imagen de un proyecto en un demonio Docker y lo ejecuta. Ejecutar esta tarea iniciará el servidor Ktor, respondiendo en http://0.0.0.0:8080 por defecto. Si su servidor está configurado para usar otro puerto, puede ajustar la asignación de puertos.
  • publishImage: compila y publica la imagen de Docker de un proyecto en un registro externo, como Docker Hub o Google Container Registry. Tenga en cuenta que debe configurar el registro externo mediante la propiedad ktor.docker.externalRegistry para esta tarea.
ktor {
    docker {
        jreVersion.set(io.ktor.plugin.features.JreVersion.JRE_11)
        localImageName.set("sample-docker-image")
        imageTag.set("0.0.1-preview")
        portMappings.set(
            listOf(
                io.ktor.plugin.features.DockerPortMapping(
                    80,
                    8080,
                    io.ktor.plugin.features.DockerPortMappingProtocol.TCP
                )
            )
        )
    }
}

Documentación

A la hora de documentar nuestro código hemos hecho uso de Dokka el cual haciendo uso de KDoc nos va a permitir comentar nuestro código y ver dicha documentación en html. Puedes ver un ejemplo completo en todo lo relacionado con Representantes (modelos, repositorios y/o servicios) y consultar la documentación en /build/dokka/html/index.html

Por otro lado se ha usado Swagger con OpenAPI para la documentación de la API. En vez de las librerías ofrecidas por el equipo de Ktor (OpenAPI y Swagger) hemos usado Ktor Swagger-UI la cual extiende el DSL de Ktor para añadir la documentación de Swagger-UI a nuestra aplicación sobre la marcha.

Puedes ver un ejemplo completo en todo lo relacionado con endpoint de Test (modelos, repositorios y/o servicios). Lo he hecho así para no llenar el proyecto de código y ser un proyecto didáctico. Puedes consultar swagger en: http://xxx/swagger/

// Put -> /{id}
put("{id}", {
    description = "Put By Id: Mensaje de prueba"
    request {
        pathParameter<String>("id") {
            description = "Id del mensaje de prueba"
            required = true // Opcional
        }
        body<TestDto> {
            description = "Mensaje de prueba de actualización"
        }
    }
    response {
        default {
            description = "Respuesta de prueba"
        }
        HttpStatusCode.OK to {
            description = "Mensaje de prueba modificado"
            body<TestDto> { description = "Mensaje de test modificado" }
        }

    }
}) {
    logger.debug { "PUT /test/{id}" }
    val id = call.parameters["id"]
    val input = call.receive<TestDto>()
    val dto = TestDto("TEST OK PUT $id : ${input.message}")
    call.respond(HttpStatusCode.OK, dto)
}

Reactividad

Como todo concepto que aunque complicado de conseguir implica una serie de condiciones. La primera de ellas es asegurar la asincronía en todo momento. Cosa que se ha hecho mediante Ktor y el uso de corrutinas.

Por otro lado el acceso de la base de datos no debe ser bloqueante, por lo que no se ha usado la librería Exposed de Kotlin para acceder a la base de datos y que trabaja por debajo con el driver JDBC. Sabemos que esto se puede podemos acercarnos a la Asincronía pura usando corrutinas y el manejo de contexto de transaccion asíncrono.

En cualquier caso, hemos decidido usar el driver R2DBC con el objetivo que el acceso a la base de datos sea no bloqueante y así poder aprovechar el uso de Flows en Kotlin y así poder usar la reactividad total en la base de datos con las corrutinas y Ktor.

reactividad

Programación reactiva: programación asíncrona de flujos observables

Programar reactivamente una api comienza desde observar y procesar las colecciones existentes de manera asíncrona desde la base de datos hasta la respuesta que se ofrezca.

Inmutabilidad

Es importante que los datos sean inmutables, es decir, que no se puedan modificar una vez creados en todo el proceso de las capas de nuestra arquitectura. Esto nos permite tener un código más seguro y predecible. En Kotlin, por defecto, podemos hacer que una clase sea inmutable, añadiendo el modificador val a sus propiedades.

Para los POKOS (Plain Old Kotlin Objects) usaremos Data Classes, que son clases inmutables por defecto y crearemos objetos nuevos con las modificaciones que necesitemos con la función copy().

Caché

La caché es una forma de almacenar datos en memoria/disco para que se puedan recuperar rápidamente. Además de ser una forma de optimizar el rendimiento, también es una forma de reducir el coste de almacenamiento de datos y tiempo de respuesta pues los datos se almacenan en memoria y no en disco o base de datos que pueden estar en otro servidor y con ello aumentar el tiempo de respuesta.

Además la caché nos ofrece automáticamente distintos mecanismos de actuación, como por ejemplo, que los elementos en cache tenga un tiempo de vida máximo y se eliminen automáticamente cuando se cumpla. Lo que nos permite tener datos actualizados Y/o los más usados en memoria y eliminar los que no se usan.

En nuestro proyecto tenemos dos repositorios, uno para la caché y otro para la base de datos. Para ello todas las consultas usamos la caché y si no está, se consulta a la base de datos y se guarda en la caché. Además, podemos tener un proceso en background que actualice la caché cada cierto tiempo solo si así lo configuramos, de la misma manera que el tiempo de refresco.

Además, hemos optimizado las operaciones con corrutinas para que se ejecuten en paralelo actualizando la caché y la base de datos.

El diagrama seguido es el siguiente

cache

Por otro lado también podemos configurar la Caché de Header a nivel de rutas o tipo de ficheros como se ha indicado.

Para este proyecto hemos usado Cache4K. Cache4k proporciona un caché de clave-valor en memoria simple para Kotlin Multiplatform, con soporte para ivalidar items basados ​​en el tiempo ( caducidad) y en el tamaño.

Notificaciones en tiempo real

Las notificaciones en tiempo real son una forma de comunicación entre el servidor y el cliente que permite que el servidor envíe información al cliente sin que el cliente tenga que solicitarla. Esto permite que el servidor pueda enviar información al cliente cuando se produzca un evento sin que el cliente tenga que estar constantemente consultando al servidor.

Para ello usaremos WebSockets junto al patrón Observer para que el servidor pueda enviar información al cliente cuando se produzca un evento sin que el cliente tenga que estar constantemente consultando al servidor.

Para ello, una vez el cliente se conecta al servidor, se le asigna un ID de sesión y se guarda en una lista de clientes conectados. Cuando se produce un evento, se recorre la lista de clientes conectados y se envía la información a cada uno de ellos, ejecutando la función de callback que se le ha pasado al servidor.

Además, podemos hacer uso de las funciones de serialización para enviar objetos complejos como JSON.

observer

Proveedor de Dependencias

Gracias al principio de inversión de dependencias (SOLID), podemos hacer que el código que es el núcleo de nuestra aplicación no dependa de los detalles de implementación, como pueden ser el framework que utilices, la base de datos, cache...Todos estos aspectos se especificarán mediante interfaces, y el núcleo no tendrá que conocer cuál es la implementación real para funcionar.

La Inyección de dependencias es un patrón de diseño que permite que las dependencias de una clase se pasen como parámetros en el constructor de la clase (principalmente). Esto nos permite que las dependencias de una clase sean independientes de la clase y que puedan ser reemplazadas por otras dependencias que implementen la misma interfaz y con ello conseguir un código no acoplado, que se adapte a cada situación y que sea fácil de testear y con ello podemos cumplir el principio de inversión de control.

Para ello usaremos Koin que es un framework de inyección de dependencias para Kotlin Multiplatform. Koin nos permite definir los módulos de inyección de dependencias y las dependencias que queremos inyectar en cada clase. En este caso hemos usado sus extensiones para Ktor y sus anotaciones para hacerlo mucho más directo.

koin

Seguridad de las comunicaciones

SSL/TLS

Para la seguridad de las comunicaciones usaremos SSL/TLS que es un protocolo de seguridad que permite cifrar las comunicaciones entre el cliente y el servidor. Para ello usaremos un certificado SSL que nos permitirá cifrar las comunicaciones entre el cliente y el servidor.

De esta manera, conseguiremos que los datos viajen cifrados entre el cliente y el servidor y que no puedan ser interceptados por terceros de una manera sencilla.

Esto nos ayudará, a la hora de hacer el login de un usuario, a que la contraseña no pueda ser interceptada por terceros y que el usuario pueda estar seguro de que sus datos están protegidos.

tsl

Autenticación y Autorización con JWT

Para la seguridad de las comunicaciones usaremos JWT que es un estándar abierto (RFC 7519) que define una forma compacta y autónoma de transmitir información entre partes como un objeto JSON. Esta información puede ser verificada y confiada porque está firmada digitalmente. Las firmas también se pueden usar para asegurar la integridad de los datos.

El funcionamiento de JWT es muy sencillo. El cliente hace una petición para autenticarse la primera vez. El servidor genera un token que contiene la información del usuario y lo envía al cliente. El cliente lo guarda y lo envía en cada petición al servidor. El servidor verifica el token y si es correcto, permite la petición al recurso.

jwt

CORS

Para la seguridad de las comunicaciones usaremos CORS que es un mecanismo que usa cabeceras HTTP adicionales para permitir que un user agent obtenga permiso para acceder a recursos seleccionados desde un servidor, en un origen distinto (dominio) al que pertenece.

cors

BCrypt

Para la seguridad de las comunicaciones usaremos Bcrypt que es un algoritmo de hash de contraseñas diseñado por Niels Provos y David Mazières, destinado a ser un método de protección contra ataques de fuerza bruta. Con este algoritmo, se puede almacenar una contraseña en la base de datos de forma segura, ya que no se puede obtener la contraseña original a partir de la contraseña almacenada.

bcrypt

Testing

Para testear se ha usado JUnit y MocKK como librerías de apoyo. Además, Hemos usado la propia api de Ktor para testear las peticiones. Con ello podemos simular un Postman para testear las peticiones de manera local, con una instancia de prueba de nuestro servicio. testear

Postman

Para probar con un cliente nuestro servicio usaremos Postman que es una herramienta de colaboración para el desarrollo de APIs. Permite a los usuarios crear y compartir colecciones de peticiones HTTP, así como documentar y probar sus APIs.

El fichero para probar nuestra api lo tienes en la carpera postman y puedes importarlo en tu Postman para probar el resultado.

postman

Distribución y Despliegue

Para la distribución de la aplicación usaremos Docker con su Dockerfile. Además, podemos usar Docker Compose con docker-compose.yml que es una herramienta para definir y ejecutar aplicaciones Docker de múltiples contenedores.

docker

Por otro lado, podemos usar JAR o Aplicaciones de sistema tal y como hemos descrito en el apartado de Despliegue.

Recuerda: Si haces una imagen Docker mete todos los certificados y recursos que necesites que use adicionalmente nuestra aplicación (directorios), si no no funcionará, pues así los usas en tu fichero de configuración. Recuerda lo que usa tu fichero de configuración para incluirlo.

Documentación

La documentación sobre los métodos se pueden consultar en HTML realizada con Dokka.

La documentación de los endpoints se puede consultar en HTML realizada con Swagger.

swagger

Recursos

Autor

Codificado con 💖 por José Luis González Sánchez

Twitter GitHub GitHub

Contacto

Cualquier cosa que necesites házmelo saber por si puedo ayudarte 💬.

                 

¿Un café?

joseluisgs




Licencia de uso

Este repositorio y todo su contenido está licenciado bajo licencia Creative Commons, si desea saber más, vea la LICENSE. Por favor si compartes, usas o modificas este proyecto cita a su autor, y usa las mismas condiciones para su uso docente, formativo o educativo y no comercial.

Licencia de Creative Commons
JoseLuisGS by José Luis González Sánchez is licensed under a Creative Commons Reconocimiento-NoComercial-CompartirIgual 4.0 Internacional License.
Creado a partir de la obra en https://github.com/joseluisgs.

About

Api REST para la gestión de tenistas usando Kotlin y Ktor. Programación de Servicios y Procesos. 2º DAM.


Languages

Language:Kotlin 99.3%Language:Dockerfile 0.3%Language:HTML 0.2%Language:Shell 0.2%