En el desarrollo de aplicaciones frontend, la gestión de las API puede ser un desafío. La interacción con múltiples endpoints, la autenticación, el manejo de errores y la sincronización de los datos son solo algunos de los aspectos a considerar. Afortunadamente, existen herramientas como Orval que pueden simplificar en gran medida esta tarea. En este artículo, explicaremos qué es Orval y cómo puede ayudarte a mejorar la gestión de API en tus proyectos frontend.

¿Qué es Orval?

Orval es una biblioteca de gestión de API para aplicaciones frontend. Está diseñada para facilitar la interacción con las API y reducir la complejidad asociada con la gestión de múltiples endpoints. Orval se integra fácilmente con frameworks populares como Vue, React y Angular, lo que la convierte en una opción versátil para proyectos frontend de diferentes tecnologías.

Principales características de Orval

Instalación

Para testear el funcionamiento de Orval, vamos a crear un proyecto de Vue, Vite y TypeScript, la configuración será similar si seleccionamos otro framework o librería como Angular o React.

npm create vite@latest orval # Seleccionaremos la opción de React y TypeScript  

Para instalar la dependencia de Orval, ejecutaremos el siguiente comando:

npm install orval --save-dev

Una vez instalado Orval, tendremos que instalar axios, ya que será necesario para realizar las integraciones con los servicios correspondientes.

npm install axios

Esta será la configuración mínima en cuanto a dependencias, podemos añadir más funcionalidad a Orval dentro de nuestro proyecto, pero por ahora nos centraremos en la generación de interfaces y servicios.

Configuración

Para configurar Orval en nuestro proyecto necesitaremos añadir un fichero orval.config.js a nuestro proyecto, en el directorio principal, o bien añadir la configuración mediante un parámetro --config una vez ejecutemos el comando. Personalmente, prefiero esta segunda opción, ya que así tendremos en el mismo directorio la configuración del servicio de Orval, las definiciones de OpenApi y las interfaces y servicios generados.

Para ello, tendremos de añadir un script a nuestro package.json similar al siguiente:

"generate:api": "orval --config src/api/config/orval.config.js"

En este ejemplo almacenaremos toda la configuración de Orval en la carpeta src/api.

El fichero de configuración debe de tener al menos un servicio definido, y podrá contener varios servicios que se definirán de la siguiente manera:

Nuestro fichero de configuración contendrá la siguiente información:

// orval.config.cjs
module.exports = {
  pokemon: {
    input: {
      target: '../definitions/pokemon/pokemon.openapi.yaml',
    },
    output: {
      target: '../generated/pokemon/pokemon.service.ts',
      schemas: '../generated/pokemon/models',
    },
  },
};

Para generar las interfaces y servicio, necesitaremos la especificación OpenApi del servicio. En este ejemplo vamos a utilizar una especificación correspondiente al API REST de PokeApi, por lo que hemos generado el fichero de definición de OpenApi para los siguientes servicios:

Así como las interfaces correspondientes a los objetos de transferencia de información.

openapi: 3.0.1
info:
  title: Pokemon Paradigma
  description: Try to test some GET enpoints
  version: 3.0.0
servers:
  - url: 'https://pokeapi.co'
paths:
  '/api/v2/pokemon':
    get:
      operationId: findAll
      description: Find all pokemon list
      summary: Finds all pokemon
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PokemonListResponse'
        '404':
          description: Error
          content:
            text/plain; charset=utf-8:
              schema:
                type: string
              examples: { }
      servers:
        - url: 'https://pokeapi.co'
    servers:
      - url: 'https://pokeapi.co'
  '/api/v2/pokemon/{id}':
    get:
      operationId: findPokemonById
      description: Enter id to find Pokemon
      summary: Finds pokemon by Id
      parameters:
        - name: id
          in: path
          description: ID of region to return
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pokemon'
        '404':
          description: Error
          content:
            text/plain; charset=utf-8:
              schema:
                type: string
              examples: {}
      servers:
        - url: 'https://pokeapi.co'
    servers:
      - url: 'https://pokeapi.co'
  '/api/v2/pokemon/{name}/':
    get:
      operationId: findPokemonByName
      description: Enter name to find Pokemon Info
      summary: Finds Pokemon by Name
      parameters:
        - name: name
          in: path
          description: The name that needs to pokemon info.
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pokemon'
        '404':
          description: Error
          content:
            text/plain; charset=utf-8:
              schema:
                type: string
              examples: {}
      servers:
        - url: 'https://pokeapi.co'
    servers:
      - url: 'https://pokeapi.co'
components:
  schemas:
    PokemonResponse:
      title: PokemonResponse
      type: object
      properties:
        name:
          type: string
        url:
          type: string
    PokemonListResponse:
      title: PokemonListResponse
      type: object
      properties:
        count:
          type: number
        next:
          type: string
        previous:
          type: string
          format: nullable
        results:
          type: array
          items:
            $ref: '#/components/schemas/PokemonResponse'
    Pokemon:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        base_experience:
          type: integer
          format: int32
        height:
          type: string
        is_default:
          type: string
        order:
          type: string
        abilities:
          type: string
        moves:
          type: string
        stats:
          type: string
        types:
          type: string

      xml:
        name: Pokemon
    Machine:
      type: object
      properties:
        id:
          type: integer
          format: int64
        item:
          type: integer
          format: int64
        move:
          type: integer
          format: int32
        version_group:
          type: string

      xml:
        name: Machine
    Region:
      type: object
      properties:
        id:
          type: integer
          format: int64
        locations:
          type: integer
          format: int64
        move:
          type: integer
          format: int32
        name:
          type: string
        names:
          type: string
        main_generation:
          type: string
        pokedexes:
          type: string
        version_group:
          type: string

      xml:
        name: Region

Generación de interfaces

Una vez configurado Orval dentro de nuestro proyecto, podremos ejecutar la generación de servicios e interfaces ejecutando el siguiente el comando:

npm run generate:api

Esto generará dentro de la ruta src/api/generated/pokemon los modelos y servicio de nuestra especificación de OpenApi.

Modelos y servicio de nuestra especificación de OpenApi.

Integración

Una vez hemos generado las interfaces y servicios podemos integrarlos en nuestro código, por ejemplo, crearemos un botón que ejecute la llamada al servicio de listado:

// components/HelloWorld.vue

import {findAll} from "../api/generated/pokemon/pokemon.service";
...
const getAll = () => {
  console.log("get all");
  findAll().then((result)=> {
    console.log(result);
  });
}
...
<button type="button" @click="getAll">Get all</button>

Una vez integrada la petición, si ejecutamos este código en nuestro navegador obtendremos el siguiente mensaje de error:

Si ejecutamos este código en nuestro navegador obtendremos el siguiente mensaje de error:

Esto se debe a que por defecto la propiedad baseUrl en axios será la url de nuestro servidor local, por tanto, tendremos que configurar la instancia de axios para asignar la url de nuestro servidor backend.

Para crear una instancia de axios, lo primero de todo tendremos que crear el archivo que la contendrá, por ejemplo axios.instance.ts. Siguiendo la documentación de Orval sobre la creación de instancias, el fichero deberá de contener el siguiente código:

// services/config/axios.instance.ts

import axios, { AxiosRequestConfig } from 'axios';

export const AXIOS_INSTANCE = axios.create({ baseURL: 'https://pokeapi.co' });

export const pokemonInstance = <T>(
  config: AxiosRequestConfig,
  options?: AxiosRequestConfig
): Promise<T> => {
  const source = axios.CancelToken.source();
  const promise = AXIOS_INSTANCE({
    ...config,
    ...options,
    cancelToken: source.token,
  }).then(({ data }) => data);
  return promise;
}

Es recomendable parametrizar el atributo baseURL para generar la url mediante variables de entorno.

Una vez creado y configurado el fichero correspondiente tendremos que asignar la instancia de axios personalizada a nuestro servicio, esta asignación se realiza dentro del fichero de configuración de Orval, en el atributo output. Añadiendo el siguiente fragmento de código:

// api/config/orval.config.js

module.exports = {
  pokemon: {
    input: {
      ...
    },
    output: {
    ...
      override: {
        mutator: {
          path: '../../services/config/axios.instance.ts',
          name: 'pokemonInstance',
        },
      },
    },
    ...
  },
};

No hay que olvidar que el nombre de la constante que exportamos en axios.instance.ts, debe ser el mismo que el definido en override.mutator.name, en nuestro caso pokemonInstance. Por lo que si volvemos a generar las interfaces y servicios, las peticiones se harán correctamente a la url del servicio.

Mocks

Una de las características de Orval es la posibilidad de añadir mocks a la hora de crear los servicios, puede ser una opción realmente útil para facilitar la integración con los servicios. Para ello, Orval se basa en la librería Mock Service Worker, que nos permitirá simular un Service Worker en entornos de desarrollo y pruebas.

Si quieres conocer cómo instalar y configurar correctamente Mock Service Worker en tu proyecto, visita el post simulando APIs para pruebas y desarrollo.

Una vez instalado y configurado MSW dentro de nuestro proyecto, procederemos a configurar Orval para generar los mocks junto con los servicios. Para ello, tendremos que modificar nuestro fichero de configuración orval.config.js y añadir la propiedad “mock” con el valor “true” dentro de output, obteniendo un bloque de código similar al siguiente:

output: {
      target: '../generated/pokemon/pokemon.service.ts',
      schemas: '../generated/pokemon/models',
      mock: true,
      override: {
        mutator: {
          path: '../../services/config/axios.instance.ts',
          name: 'customInstance',
        },
      },
    }

Una vez ejecutado nuevamente el script de generación, podremos observar cómo en nuestro fichero pokemon.service.ts se han generado cuatro nuevas exportaciones.

Estos mocks, por sí solos, no tienen ninguna funcionalidad. En el caso de Mock Service Worker, será necesario registrarlo en su fichero de configuración de la siguiente manera:

// mock.config.ts
import { RequestHandler } from 'msw';
import { setupWorker } from 'msw/browser';
import {getPokemonParadigmaMock} from "../../api/generated/pokemon/pokemon.service.ts";

const mocks: Array<RequestHandler> = [];

const loadHandler = () => {
    // Añadir aquí los servicios mockeados
    mocks.push(...getPokemonParadigmaMock());
};

const initMocks = () => {
    loadHandler();
    const worker = setupWorker(...mocks);
    worker.start();
};

export default initMocks;

Hooks

Orval dispone de una configuración de hooks propia para poder ejecutar tareas automatizadas una vez ha finalizado la generación de ficheros, por ejemplo aplicar las reglas de prettier de nuestro proyecto al código autogenerado.

Para ello tendremos que añadir el siguiente código a nuestro fichero de configuración:

module.exports = {
  pokemon: {
    input: {
...
    },
    output: {
...
    },
    hooks: {
      afterAllFilesWrite: 'npx prettier --write ./src/api/generated',
    },
  },
};

De este modo al finalizar la generación se ejecutará el comando “npx prettier --write ./src/api/generated”, esto es solamente un ejemplo de tareas que podemos realizar con los hooks dentro de nuestro proyecto.

Conclusión

Orval es una biblioteca potente y versátil que simplifica la gestión de API en aplicaciones frontend. Proporciona una configuración sencilla, generación automática de clientes, gestión de autenticación, interceptores de peticiones y respuestas, y generación de documentación. Estas características hacen que Orval sea una herramienta valiosa para cualquier desarrollador frontend que desee optimizar el flujo de trabajo de las API y mejorar la eficiencia en el desarrollo de aplicaciones.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete