đź“š DĂ©veloppement du backend

De nombreux frameworks supportent officiellement Kotlin comme Springopen in new window, Quarkusopen in new window et Ktoropen in new window, parmi d'autres listés iciopen in new window.

En outre, Kotlin est théoriquement compatible avec tout framework qui cible la JVM ou JS. Cependant, les frameworks qui ne supportent pas officiellement Kotlin peuvent nécessiter quelques ajustements pour l'utiliser.

Ktor

Ktor est une bibliothèque Kotlin multiplateforme permettant de développer des clients et des serveurs HTTP. Cela fait de Ktor une bibliothèque utile à la fois aux développeurs frontend, pour la partie client HTTP, ainsi qu'aux développeurs backend, pour la partie serveur HTTP. Dans ce qui suit, nous allons créer une API REST avec le serveur Ktor.

TP : développer une API avec Ktor

  • CrĂ©ez un projet sur start.ktor.ioopen in new window avec les plugins suivants : Content Negotiation, kotlinx.serialization, et Routing.
  • Cliquez sur "Generate project".
  • TĂ©lĂ©chargez l'archive, dĂ©compressez-la et ouvrez le projet avec votre IDE prĂ©fĂ©rĂ©.
  • CrĂ©ez un package models et ajoutez-y une classe de donnĂ©es Customer avec ces propriĂ©tĂ©s immuables id : String, firstName : String, lastName : ChaĂ®ne, email : ChaĂ®ne.
  • Annotez la classe avec @Serializable.
  • CrĂ©ez un nouveau package nommĂ© routes et ajoutez-y un fichier CustomerRoutes.kt qui contiendra le code pour l'endpoint /customer.
  • Le code ci-dessous fournit l'implĂ©mentation de certains endpoints. Veuillez implĂ©menter les autres.
  • Pour activer la route, appelez customerRouting() dans le fichier de configuration du routage situĂ© dans plugins/Routing.kt.
  • Pour plus de simplicitĂ©, utilisez une liste globale de clients en mĂ©moire val store = mutableListOf<Customer>().
  • Lancer le serveur en exĂ©cutant la mĂ©thode main.
  • Tester l'API sur l'IDE en utilisant un fichier http ou en utilisant n'importe quel autre client.
CustomerRoutes.kt
val store = mutableListOf<Customer>()

fun Route.customerRouting() {
  route("/customer") {
    get {
      call.respond(store)
    }
    get("{id?}") {
      val id = call.parameters["id"] ? : return@get call.respondText(
        "Missing id",
        status = HttpStatusCode.BadRequest
      )
      val customer =
        store.find { it.id == id } ? : return@get call.respondText(
          "Pas de client avec l'id $id",
          status = HttpStatusCode.NotFound
        )
      call.respond(customer)
    }
    post {
      val customer = call.receive<Customer>()
      store.add(customer)
      call.respondText("Customer stored correctly", status = HttpStatusCode.Created)
    }
    delete("{id?}") {
      // TODO
    }
  }
}

plugins/Routing.kt
fun Application.configureRouting() {
    routing {
        customerRouting()
    }
}

return@label

Vous pouvez spécifier le niveau que vous voulez retourner avec un label explicite en utilisant return@lambda.

lambdaA {
    lambdaB {
        lambdaC {
            val randomInt = Random.nextInt(0, 100)
            if (randomInt > 50) return@lambdaC else return@lambdaB
        }
        printf("In lambdaB")
    }
}

Ce code exécute un autre exempleopen in new window.

CustomerTest.http
POST http://127.0.0.1:8080/customer
Content-Type : application/json

{
  "id" : "100",
  "firstName" : "Jane",
  "lastName" : "Smith",
  "email" : "jane.smith@company.com"
}


###
POST http://127.0.0.1:8080/customer
Content-Type : application/json

{
  "id" : "200",
  "firstName" : "John",
  "lastName" : "Smith",
  "email" : "john.smith@company.com"
}

###
POST http://127.0.0.1:8080/customer
Content-Type : application/json

{
  "id" : "300",
  "firstName" : "Mary",
  "lastName" : "Smith",
  "email" : "mary.smith@company.com"
}


###
GET http://127.0.0.1:8080/customer
Accept : application/json

###
GET http://127.0.0.1:8080/customer/200
Accepte : application/json

###
GET http://127.0.0.1:8080/customer/500
Accepte : application/json

###
DELETE http://127.0.0.1:8080/customer/100

###
DELETE http://127.0.0.1:8080/customer/500

Cette page contient des étapes détailléesopen in new window

nodejs

Grâce à Kotlin/JS, nous pouvons écrire des applications qui ciblent nodejs en utilisant Kotlin.

On peut même importer des librairies npm à condition de déclarer les API JS que l'on va utiliser en Kotlin. C'est ce qu'on appelle une déclaration externe (vous pouvez la considérer comme un équivalent des définitions de type de TypeScript) qui déclare les symboles auxquels nous voulons accéder en Kotlin grâce aux annotations @JsModuleopen in new window et @JsNonModuleopen in new window. Définir de telles déclarations externes peut s'avérer fastidieux et il ne semble pas y avoir de générateur automatique officiel et stable (dukatopen in new window a été supprimé dans kotlin 1.8.20). Dans ce cas, nous avons deux options, soit écrire la déclaration externe nous-même, soit l'importer en tant que dépendance si elle est disponible.

Heureusement pour nous, le prochain TP utilise la librairie Express pour laquelle nous pouvons trouver une déclaration de type externe.

TP : API Rest avec Kotlin/JS et Express

implementation(npm("express", "> 4.0.0 < 5.0.0"));
implementation("dev.chriskrueger:kotlin-express:1.2.0");
  • Modifiez main.kt comme suit. Cela crĂ©e un serveur API REST qui Ă©coute le port 3000 et fournit une route GET /hello.
data class Message(val id : Int, val message : String)

fun main() {
    val messages = listOf(Message(0, "I love Kotlin/JS"))
    val app = express.Express()
    app.get("/hello") { req, res ->
        res.send(messages)
    }

    app.listen(3000) {
        console.log("server start at port 3000")
    }
}
  • ExĂ©cutez la tâche nodeRun depuis votre IDE ou depuis la ligne de commande (si vous avez installĂ© Gradle).
    • Si vous rencontrez une erreur avec Yarn lock, exĂ©cutez la tâche kotlinUpgradeYarnLock puis rĂ©essayez.
  • Ajouter des routes en POST, PUT et DELETE
  • En ce qui concerne le corps du POST, Express positionne req.body Ă  undefined Ă  moins que nous ne spĂ©cifions un body parser.

Spring framework

Spring est un framework célèbre pour le développement d'applications côté serveur : API REST, pages web générées par le serveur, microservices, etc. Il s'appuie sur l'écosystème Java pour la compilation et l'exécution, ce qui le rend compatible avec Kotlin. Mieux encore, Spring supporte officiellement Kotlin. On peut même démarrer un nouveau projet avec Kotlin et Gradle-Kotlin. Dans la prochaine section, nous utiliserons ce projet pour recréer notre API REST plus haut avec Spring.

TP : Spring boot part 1 - développer la même API avec Spring Boot

  • CrĂ©ez un projet sur start.spring.io (aussi appelĂ© Spring initializr)open in new window avec les dĂ©pendances suivantes : Spring Web et Spring Boot DevTools.
  • Choisissez Kotlin comme langage et Kotlin-Grade comme gestionnaire de projet.
  • Ajoutez les dĂ©pendances suivantes : Spring Web et Spring Boot DevTools.
  • Cliquez sur Generate. TĂ©lĂ©chargez l'archive, dĂ©compressez-la et ouvrez le projet avec IntelliJ (de prĂ©fĂ©rence) ou VSCode.
  • VĂ©rifiez que la partie plugins build.gradle.kts utilise la dernière version de Kotlin. Voici Ă  quoi cela devrait ressembler avec Kotlin 1.8.10 :
plugins {
  id("org.springframework.boot") version "3.0.4"
  id("io.spring.dependency-management") version "1.1.0"
  kotlin("jvm") version "1.8.10"
  kotlin("plugin.spring") version "1.8.10"
}
  • CrĂ©ez la data class Customer dans le package model (sans l'annotation @Serializable).
  • CrĂ©ez un paquetage controller qui contient une classe CustomerController qui fournit un CRUD en utilisant une liste globale.
    • Vous pouvez trouver un squelette ci-dessous.
    • đź’ˇ Dans Spring, les contrĂ´leurs Rest servent de routes Ktor, oĂą un contrĂ´leur dĂ©finit une ressource REST.
  • DĂ©finissez les mĂŞmes routes que dans le TP prĂ©cĂ©dent.
  • DĂ©marrez le serveur de l'API REST en exĂ©cutant :
    • Sur Powershell : .\gradlew.bat bootRun
    • Tout shell Unix : .\gradlew bootRun
    • Ou bien, vĂ©rifiez si votre IDE fournit dĂ©jĂ  des configurations d'exĂ©cution pour les projets Spring Boot.
  • Veuillez tester les routes avec un client REST. Vous pouvez trouver des fichiers http ici au format JetBrainsopen in new window ou au format de l'extension REST Client de VSCodeopen in new window
CustomerController.kt
val store = mutableListOf<Customer>()

@RestController
@RequestMapping("/customer")
class CustomerController {
    @GetMapping
    fun getAll() = store

    @GetMapping("{id}")
    fun getById(@PathVariable id : String) { /* TODO : implement */ }

    @PostMapping
    fun addOne(@RequestBody customer : Customer) { /* TODO : implement */ }

    @DeleteMapping("{id}")
    fun deleteOne(@PathVariable id : String) { /* TODO : implement */ }
}

TP : Spring boot partie 2 - ajouter une base de données

Allons un peu plus loin en stockant des données dans une base de données et en écrivant quelques tests.

Nous utiliserons la base de données en mémoire H2 pour des raisons de simplicité, puisqu'elle ne nécessite pas de serveur pour fonctionner. Les classes seront mappées aux tables de la base de données avec des annotations JPA. L'API de base de données que nous utiliserons s'appelle JPARepository. C'est une API légère qui fournit des fonctionnalités CRUD communes à partir d'une simple une interface.

  • CrĂ©ez un nouveau projet Spring en utilisant Spring initializropen in new window avec Kotlin et les dĂ©pendances suivantes : Spring Data JPA, H2 Database, Spring Boot DevTools, Spring Web.
  • Ouvrez le projet et ajoutez cette classe dans le package model @Entity class Product(@Id @GeneratedValue var id : Long ? = null, var name : String, var price : Int). Ceci dĂ©finit la classe ainsi que les annotations JPA minimales (@Entity, @Id et @GeneratedValue) pour gĂ©nĂ©rer la table correspondante.
  • Dans le package repository, dĂ©clarez l'interface ProductRepository comme suit interface ProductRepository : JpaRepository<Produit, Long>. C'est suffisant pour que Spring gĂ©nère une implĂ©mentation avec des caractĂ©ristiques communes comme nous le verrons plus tard.
  • Ensuite, crĂ©ez une classe ProductService qui contiendra la logique mĂ©tier. En termes d'architecture, le contrĂ´leur appelle un service qui, Ă  son tour, s'appuie sur d'autres services ou rĂ©fĂ©rentiels.
ProductService.kt
@Service
class ProductService(@Autowired val productRepository: ProductRepository) {
    fun getAll() = productRepository.findAll()

    // use findByIdOrNull instad of findById because the latter returns an optional<Product> instead of Product?
    fun getById(id: Long) = productRepository.findByIdOrNull(id)
}
  • Dans le package controller, crĂ©ez une classe ProductController qui est mappĂ©e Ă  /product et injectĂ©e avec @Autowired. RĂ©pondez Ă  @Get comme suit.
ProductController.kt
@RestController
@RequestMapping("/product")
class ProductController(@Autowired val productService : ProductService) {
    @GetMapping fun getAll() = productService.getAll()

    @GetMapping("{id}")
    fun getById(@PathVariable id : Long) =
        productService.getById(id) ? : throw ResponseStatusException(HttpStatus.NOT_FOUND)
}

Kotlin rend getById(@PathVariable id : Long) plus concis

L'opérateur Elvis ?: permet de simplifier le code. Voici une version plus longue en guise de référence.

@GetMapping("{id}")
fun getById(@PathVariable id : Long) : Produit {
    val product = productService.getById(id)
    if (product != null){
        return product
    }
    throw ResponseStatusException(HttpStatus.NOT_FOUND)
}

En outre, Spring fournit @ControllerAdvice pour modifier le message d'exception. Vous pouvez voir un [exemple ici] (https://spring.io/guides/tutorials/rest/).

  • ExĂ©cutons le projet. Avant de lancer le projet, nous devons ajouter un plugin qui permet aux classes Kotlin de gĂ©nĂ©rer un constructeur par dĂ©faut id("org.jetbrains.kotlin.plugin.jpa") version "1.8.10". Les plugins devraient ressembler Ă  ce qui suit :
plugins {
  id("org.jetbrains.kotlin.plugin.jpa") version "1.8.10"
  id("org.springframework.boot") version "3.0.4"
  id("io.spring.dependency-management") version "1.1.0"
  kotlin("jvm") version "1.8.10"
  kotlin("plugin.spring") version "1.8.10"
}

  • En guise d'exercice, implĂ©mentez ces routes : POST d'un seul produit, DELETE par id (/produit/{id}) et GET par id (/produit/{id}).
    • Indice : ProductController fournit dĂ©jĂ  les mĂ©thodes nĂ©cessaires.
  • Appelez les diffĂ©rents points de terminaison avec un client REST.
  • Tester votre API Rest avec un client HTTP

TP : Spring boot partie 3 - ajouter des tests

Les frameworks Spring permettent d'effectuer différents types de tests en fournissant différentes classes dès le départ :

  • Tests unitaires/de composants des services et de l'API REST. Cela se fait par le biais d'utilitaires de bouchonnage tels que MockMVC.
  • Tests d'intĂ©gration de l'API REST en utilisant TestRestTemplate. Dans ce cas, un serveur complet est exĂ©cutĂ© et testĂ©.

La plupart des classes fournies par Spring, si ce n'est toutes, offrent une syntaxe élégante pour les développeurs Java. Certaines d'entre elles vont plus loin en tirant parti des caractéristiques spécifiques de Kotlin. Dans ce qui suit, nous allons nous concentrer sur les parties qui fournissent des DSLs Kotlin, à savoir le test unitaire de l'API REST avec MockMVC.

  • CrĂ©er une classe de test ProductControllerUnitTests avec le contenu initial ci-dessous. MockMvc permet de tester unitairement l'API REST. L'annotation @AutoConfigureMockMvc permet Ă  Spring de la configurer automatiquement.
@SpringBootTest
@AutoConfigureMockMvc
classe ProductControllerTests(
    @Autowired val mockMvc : MockMvc,
    @Autowired val productRepository : ProductRepository) {

    @BeforeEach
    fun reset(){
        productRepository.deleteAll()
    }
}
  • Ajoutez les deux tests ci-dessous. Le premier utilise une approche classique tandis que le second tire parti des capacitĂ©s du DSL de Kotlin. De plus, nous utilisons une chaĂ®ne littĂ©rale plus lisible.
@Test
fun testWithClassicApproach(){
    mockMvc.perform(get("/product"))
        .andExpect(status().isOk)
        .andExpect(content().string(containsString("[]")))
}
@Test
fun `test GET a single product`() {
    mockMvc.get("/product/1").andExpect {
        status { isOk() }
        jsonPath("$.name") { value("A") }
        jsonPath("$.price") { value(1) }
        content { contentType(MediaType.APPLICATION_JSON) }
    }
}
  • En guise d'exercice, Ă©crire des tests pour les autres points d'accès.

Le constructeur de requĂŞtes de JpaRepository

Les repository Spring implémentent des requêtes basées sur le nom de leurs méthodes. Par exemple, pour obtenir tous les produits triés par nom, nous pouvons ajouter cette méthode à l'interface.

interface ProductRepository : JpaRepository<Produit, Long> {
    fun findAllByOrderByNameAsc() : List<Produit> ;
}

La [documentation officielle] (https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation) fournit des explications et des exemples plus détaillés.

Projets terminés

Aller plus loin

Ces tutoriels officiels vont encore plus loin :

Lien et références