Utiliser OpenAPI dans une application Nuxt

By Hugo LassiègeJul 5, 20245 min read

Parlons d'API.

Une API, vous en utilisez tous les jours quand vous êtes développeur/développeuse. C'est une ou des url proposé par un service et qui vous permet de récupérer de l'information ou déclencher des traitements.

Par exemple

  • l'api d'accuweather pour récupérer la météo d'un lieu
  • l'api de Google drive pour lire ou écrire des documents

etc...

Et bien sûr, si vous développez une application, vous avez sans doute une API entre votre frontend, et votre backend.

Evidemment sur Malt, on a des API et récemment, on a fait disparaitre une large part du code Frontend en utilisant OpenAPI (plus de 80% du code frontend a disparu sur certaines applications).

C'est typiquement un choix technologique que j'ai conservé sur RssFeedPulse et mes prochains SAAS.

Laissez-moi vous expliquer.

Comment appeler une API (dans une application Nuxt)

Reprenons un peu la base. Voici le code d'appel d'une API, en utilisant $fetch :

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

async function addTodo() {
  const todo = await $fetch<Todo>('/api/todos', {
    method: 'POST',
    body: {
      title: 'something',
      completed : false   
    }
  })
}

Analysons un peu cet appel :

  • /api/todos est l'url de l'api sur le serveur. C'est hardcodé côté front. Si le backend change son url, il faudra penser à mettre à jour cette valeur
  • la méthode POST est également harcodé. Si jamais, côté backend, on décide que finalement c'est un PUT parce que c'est une modification, pareil, il faudra mettre à jour côté frontend.
  • le body contient des informations à envoyer côté serveur. Ces informations vont peut-être varier un jour. Le nom peut varier. Le type peut varier. etc...
  • j'ai pu préciser le type de retour : Todo. Ce type pourra évoluer dans le futur pour contenir une date de complétion. Les propriétés pourront changer de nom également.

Dans ce simple appel, je me rends compte que j'ai déjà 4 parts variables qui pourront changer dans le temps. Chaque modification côté backend devra entraîner une mise à jour côté frontend.

Au-delà de ça, j'ai pas mal de lignes qui sont sans doute une duplication de la déclaration de mon api côté backend. La duplication c'est pas toujours un problème, mais ça peut être source d'erreur.
Voici à quoi ça peut ressembler côté backend.

@Entity
data class Todo(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val title: String,
    val completed: Boolean = false
)

@RestController
@RequestMapping("/api/todos")
class TodoController(private val todoService: TodoService) {

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun createTodo(@RequestBody todo: Todo): Todo {
        return todoService.addTodo(todo)
    }
}
TIP

C'est uniquement pour simplifier que je renvoie directement l'entité Todo côté frontend. En théorie on utilise des objets de transfert (DTO).

Et si on faisait un peu différemment. Et si on essayait :

  • d'éviter les duplications, par exemple la déclaration du type Todo
  • de s'éviter tout les soucis classiques côté frontend (est-ce que je dois utiliser un POST ou un PUT pour cette API ? Est-ce que c'est un query param ou un body qui est attendu ici etc...)

C'est le moment de parler d'OpenAPI.

OpenAPI

OpenAPI est une spécification qui permet de définir une API. On peut préciser les urls utilisés, les paramètres, les types de requêtes, les codes retours etc...

Voici par exemple la spec OpenAPI de notre api de todo :

openapi: 3.0.0
info:
  title: Exemple d'API
  description: Une simple API pour les tâches Todo
  version: 1.0.0
paths:
  /todos:
    post:
      tags:
        -  "todo-api"
      operationId: "createTodo"        
      summary: Crée une nouvelle tâche
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Todo'
      responses:
        '201':
          description: La tâche créée
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Todo'
components:
  schemas:
    Todo:
      type: object
      properties:
        id:
          type: integer
          format: int64
        title:
          type: string
        completed:
          type: boolean

Bonne nouvelle, vous pouvez générer automatiquement ce fichier et l'exposer sur une url si vous utilisez Spring boot.

Bref, ce fichier est automatiquement exposé par mon application Spring Boot qui s'appuie sur tout les controlleurs que j'ai écrit.

TIP

À noter qu'on peut aussi partir du fichier yaml écrit à la main pour générer l'api. Mais je ne détaillerai pas ici cette approche. Elle a cependant de nombreux avantages, notamment en équipe.

Utiliser OpenAPI pour simplifier le code frontend

Si on récapitule ce qu'on vient de voir côté backend :

  • on a un controller qui définit notre service de TODO
  • on a un contrat OpenAPI exposé automatiquement par notre serveur

Et si on réutilisait ce contrat pour générer notre code d'appel frontend ?

Pour ça, on va utiliser un générateur de code lié au projet openApi.

On ajoute la dépendance sur notre application :

package.json
  "devDependencies": {
    ...
    "@openapitools/openapi-generator-cli": "2.13.4",
    ...
  }

Et on rajoute un script :

package.json
  "scripts": {
    ...
    "generate:client-api": "openapi-generator-cli generate -i http://localhost:8080/api-docs.yaml -g typescript-fetch -o ./openapi/",
    ...
  },

Désormais, on peut appeler la commande generate:client-api à chaque fois qu'on veut rafraichir notre client API. Cet outil va appeler l'url qui est sur notre serveur backend (qui doit donc tourner en local) et va générer du code dans le répertoire openapi

npm run generate:client-api
TIP

Cette méthode de travail me convient bien, étant seul à travailler avec moi-même.
Forcément, il faudrait adapter ce flux de travail pour une équipe afin d'éviter les conflits. Voire automatiser une partie des actions.

Maintenant, je peux appeler l'api Todo très facilement :

const api = new TodoApi(new Configuration({basePath: '/'}))
const todo = await api.createTodo( {title : 'something', completed : false})

Le client généré, avec Typescript, nous permet ici de ne plus nous soucier, de l'url du service, de la méthode http à utiliser, du type de paramètre en entrée ou en retour.

Si le backend change, il suffit de relancer la commande npm run generate:client-api et de vérifier si tout compile encore avec typescript.

Qu'est-ce que j'y ai gagné ?

  • de la concision côté frontend (>80% du code économisé)
  • une certaine forme de tranquilité d'esprit. Si le code backend change, je sais facilement mettre à jour mon frontend et répérer les incomptabilités avec Typescript.

Et la conséquence de tout ça, j'ai gagné du temps.


Share this:

Written by Hugo Lassiège

Software engineer, ex-freelance, ex-cofounder, ex-CTO. I love building things, sharing knowledge and helping others.

Copyright © 2024
 Eventuallycoding
  Powered by Bloggrify