Beta Acid logo

Orval

The Secret to Frontend-Backend Coordination
Development

Guilherme Mierzwa

October 26, 2023

23 min read

Introduction

In our recent article about API Contracts we went over how OpenAPI can be used to improve coordination between frontend and backend. While the specification itself is already helpful, additional tools are required in order to leverage its full potential. One such tool is Orval, a javascript library capable of generating client code with appropriate type-signatures from any valid OpenAPI v3 or Swagger v2 specification.

Orval is able to generate not only the basic requests, but also types for every query parameter, path parameter and response object. This type-safe approach simplifies development by showing exactly what values can be sent or received from an endpoint, before executing or compiling any code. Another very important feature of Orval is mock generation, which allows code to be tested without the need for actual API calls. Also, since no API calls are required for those tests, the frontend team is able to validate their changes before the endpoint's completion.

Getting started

There are 2 basic requirements to start using Orval in your project:

  • Make sure you have a valid API specification.

This can be either a link to a specification you’re hosting or a local file, both json and yaml formats are accepted. To make sure your specification is valid head over to Swagger Editor and paste it there. If the editor complains about anything, fix that before moving forward, as that’ll prevent Orval from working properly.

  • Install Orval in your project.

To do that simply run the install command using your preferred package manager, such as:

npm install orval

With that in place you’re ready to run Orval. For that, we’ll be using this simple API as an example:

openapi: "3.0.0"
info:
  version: 1.0.0
  title: Swagger Petstore
  license:
    name: MIT
paths:
  /pet:
    post:
      summary: Add a pet
      operationId: addPet
      tags:
        - pets
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                tag:
                  type: string
      responses:
        '200':
          description: The created pet data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Pet"
  /pets:
    get:
      summary: List all pets
      operationId: listPets
      tags:
        - pets
      parameters:
        - name: limit
          in: query
          description: How many items to return at one time (max 100)
          required: false
          schema:
            type: integer
            maximum: 100
            format: int32
      responses:
        '200':
          description: A paged array of pets
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Pet"


components:
  schemas:
    Pet:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        tag:
          type: string

The command to run Orval is:

$ orval --input ./api.yaml --output ./src/output.ts

Keep in mind that the output file will be overwritten every time you run this command

In our example, the generated code looks like this:

/**
 * Generated by orval v6.19.0 🍺
 * Do not edit manually.
 * Swagger Petstore
 * OpenAPI spec version: 1.0.0
 */
import axios from 'axios'
import type {
  AxiosRequestConfig,
  AxiosResponse
} from 'axios'
export type ListPetsParams = {
/**
 * How many items to return at one time (max 100)
 */
limit?: number;
};


export type AddPetBody = {
  name?: string;
  tag?: string;
};


export interface Pet {
  id: number;
  name: string;
  tag?: string;
}



  /**
 * @summary Add a pet
 */
export const addPet = <TData = AxiosResponse<Pet>>(
    addPetBody: AddPetBody, options?: AxiosRequestConfig
 ): Promise<TData> => {
    return axios.post(
      `/pet`,
      addPetBody,options
    );
  }


/**
 * @summary List all pets
 */
export const listPets = <TData = AxiosResponse<Pet[]>>(
    params?: ListPetsParams, options?: AxiosRequestConfig
 ): Promise<TData> => {
    return axios.get(
      `/pets`,{
    ...options,
        params: {...params, ...options?.params},}
    );
  }


export type AddPetResult = AxiosResponse<Pet>
export type ListPetsResult = AxiosResponse<Pet[]>

The generated code includes a Typescript function for each API endpoint, that takes all the parameters defined on the specification, makes an axios request and returns the result. Both the parameters and the results are typed, allowing issues to be caught before any API call is made.

Configuration

While it’s possible to run Orval using only --input and --output, as shown above, to access the library’s full potential you’ll need a configuration file. Using a configuration file allows you to define multiple APIs at once, each having its own settings, as well as custom HTTP clients, mocks, overrides and more.

The configuration file is normally called orval.config.ts and the equivalent to the command we’ve used previously would be:

module.exports = {
  petstore: {
    input: './api.yaml',
    output: './src/output.ts',
  },
};

The command to run Orval using this file becomes:

$ orval --config ./orval.config.js

Or simply:

$ orval

On the first level of the exported object we have defined petstoreas a key and its value is the configuration for that API. If the application needs to access multiple APIs that have OpenAPI definitions, we can update the configuration like this:

module.exports = {
  petstore: {
    input: './api.yaml',
    output: './src/output.ts',
  },
  otherApi: {
    input: 'https://petstore.swagger.io/v2/swagger.json',
    output: './src/otherApiOutput.ts',
  },
};

Some other parameters worth noting include:

  • baseUrl - allows you to add a part of the url that's been omitted from the specification or set the entire server url as a prefix for all operations;
  • workspace - places all generated code in a separate folder
  • schemas - separates types and interfaces into their own files on a different folder
  • prettier - runs prettier on the generated code, in order to keep it consistent with the rest of your application
  • tslint - runs tslint on the generated code

This is what a more complex configuration would look like, using the parameters described above:

module.exports = {
  petstore: {
    input: {
      target: './api.yaml',
    },
    output: {
      workspace: './src/orval/petstore',
      schemas: './model',
      target: './services',
      prettier: true,
      tslint: true,
      baseUrl: 'https://petstore.swagger.io/v2',
    },
  },
};

Running Orval again with this configuration results in the following file structure:

└── src
    └── orval
        └── petstore
            ├── model
            │   ├── addPetBody.ts
            │   ├── index.ts
            │   ├── listPetsParams.ts
            │   └── pet.ts
            ├── services
            │   └── swaggerPetstore.ts
            └── index.ts

There are now separate files for each type and interface, as well as a single file under services, which contains the actual request code. This separation can help find specific types and facilitates PR reviews, since the changes will be isolated. Also, while the structure changed, the only code difference is that the requests now contain the full URL. So instead of just /pet or /pets the baseUrl is included in the requests, as shown in the swaggerPetstore.ts file:

/**
 * Generated by orval v6.19.0 🍺
 * Do not edit manually.
 * Swagger Petstore
 * OpenAPI spec version: 1.0.0
 */
import axios from 'axios';
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import type { AddPetBody, ListPetsParams, Pet } from '../model';

/**
 * @summary Add a pet
 */
export const addPet = <TData = AxiosResponse<Pet>>(addPetBody: AddPetBody, options?: AxiosRequestConfig): Promise<TData> => {
  return axios.post(`https://petstore.swagger.io/v2/pet`, addPetBody, options);
};

/**
 * @summary List all pets
 */
export const listPets = <TData = AxiosResponse<Pet[]>>(params?: ListPetsParams, options?: AxiosRequestConfig): Promise<TData> => {
  return axios.get(`https://petstore.swagger.io/v2/pets`, {
    ...options,
    params: { ...params, ...options?.params }
  });
};

export type AddPetResult = AxiosResponse<Pet>;
export type ListPetsResult = AxiosResponse<Pet[]>;

These are just some of the configuration options available for the library. The complete documentation on what parameters are available and how to configure them can be found here. In the following sections we’ll go over some other parameters and examples that might be useful for different applications.

HTTP Client

By default, the client code generated by Orval utilizes axios, but there are also different options depending on the framework and libraries you’re using. The options currently available are: axios, axios-functions, angular, react-query, svelte-query, vue-query, swr and zod. To use a different client, simply set the client property under the output configuration. Some sample applications that make use of these can be found under the repository’s samples folder.

Besides the predefined options, custom clients can also be used with Orval. This allows HTTP clients that aren’t supported by default to be utilized, such as fetch, as well as the creation of custom instances for existing clients. With custom clients it's possible to order to handle authentication, headers or pre-process the requests and responses in some other way. Below we can find an example on how to use a custom axios instance:

import Axios, { AxiosRequestConfig } from 'axios';
import auth from '@firebase/auth';
import qs from 'qs';

export const AXIOS_INSTANCE = Axios.create({
  baseURL: process.env.BASE_API_URL
});

// Make sure this function receives 2 arguments.
// The first argument is set by orval and the second one allows custom configs to be passed when making the request.
export const customInstance = async <T>(config: AxiosRequestConfig, options?: AxiosRequestConfig): Promise<T> => {
  const token = await auth.getAuth().currentUser?.getIdToken();

  const promise = AXIOS_INSTANCE({
    ...config,
    ...options,
    headers: {
      ...config.headers,
      ...options?.headers,
      authorization: token ? `Bearer ${token}` : '',
    },
    withCredentials: true,
    paramsSerializer: (params) => {
      return qs.stringify(params, { arrayFormat: 'repeat' });
    }
  })
    .then((res) => res.data)
    .catch((err) => {
      if (err.response) {
        const { data, status } = err.response;
        let errorMessage = data?.message || data?.detail || err.response.statusText;
        if (status === 422) {
          errorMessage = data?.detail[0].msg;
        }

        // If backend error is detected, we extract the error message and throw a new error for react-query to handle
        throw new Error(errorMessage);
      }
      // Always throw the error so it can reach react query
      throw err;
    });

  return promise;
};

There are a few things worth noting in this custom axios instance:

  • We’re setting the baseURL parameter, this is another way of changing the base URL for all requests. We could also set other parameters that will be used on all requests, such as headers, transformers and much more.
  • Before each request is made, we have a call being made to the @firebase/auth library to get the current user’s ID token, which will then be used as an authorization header. This code could be easily modified in order to support different authorization methods that should apply to all requests.
  • Besides the authorization header, we’re also passing some other basic options to all requests, such as the paramsSerializer. With this it’s possible to change the format of arrays and other query params to meet the API’s requirements.
  • Lastly, instead of just throwing the error we get from axios as is, we can create custom errors based on the error payload sent by the API.

Now, in order to actually use this custom instance we’ll need to set the mutator parameter on orval.config.ts:

module.exports = {
  petstore: {
    input: {
      target: './api.yaml',
    },
    output: {
      workspace: './src/orval/petstore',
      schemas: './model',
      target: './services',
      prettier: true,
      tslint: true,
      baseUrl: 'https://petstore.swagger.io/v2',
      override: {
        mutator: {
          path: '../utils/axios.ts',
          name: 'customInstance'
        },
      },
    },
  },
};

Beware that path is relative to the workspace

The code we get as a result of running Orval with this new configuration is:

/**
 * Generated by orval v6.19.0 🍺
 * Do not edit manually.
 * Swagger Petstore
 * OpenAPI spec version: 1.0.0
 */
import type { AddPetBody, ListPetsParams, Pet } from '../model';
import { customInstance } from '../../../utils/custom-instance';

// eslint-disable-next-line
type SecondParameter<T extends (...args: any) => any> = T extends (config: any, args: infer P) => any ? P : never;

/**
 * @summary Add a pet
 */
export const addPet = (addPetBody: AddPetBody, options?: SecondParameter<typeof customInstance>) => {
  return customInstance<Pet>(
    { url: `https://petstore.swagger.io/v2/pet`, method: 'post', headers: { 'Content-Type': 'application/json' }, data: addPetBody },
    options
  );
};

/**
 * @summary List all pets
 */
export const listPets = (params?: ListPetsParams, options?: SecondParameter<typeof customInstance>) => {
  return customInstance<Pet[]>({ url: `https://petstore.swagger.io/v2/pets`, method: 'get', params }, options);
};

export type AddPetResult = NonNullable<Awaited<ReturnType<typeof addPet>>>;
export type ListPetsResult = NonNullable<Awaited<ReturnType<typeof listPets>>>;

As expected, instead of making calls directly to axios, this new code calls our customInstance function instead. Also, notice how the calls made to customInstance always have 2 arguments, which is why we need to define both config and options as arguments when creating a custom instance.

Mocks

Orval is not only capable of generating code to make requests for all endpoints of your API, but also mocks for every one of those endpoints too. The mock library used is called Mock Service Worker (msw), which intercepts requests made during tests on a network level, avoiding the need to mock HTTP clients or other libraries. Lastly, mocks generated by Orval are not only valuable to reduce the time spent writing tests, but also to allow breaking changes made to the API to be detected more easily, since they will match the latest changes made to the API.

To enable mock generation with Orval we simply need to set the mock property to true in the configuration. You can also set the mode property to split, which causes the generated mocks to be placed on a separate file. By default, Orval will save the mocks in the same file as the API requests. Expanding on our previous examples, a configuration file with mocks would look something like this:

module.exports = {
  petstore: {
    input: {
      target: './api.yaml',
    },
    output: {
      workspace: './src/orval/petstore',
      schemas: './model',
      target: './services',
      prettier: true,
      tslint: true,
      mock: true,
      mode: 'split',
      baseUrl: 'https://petstore.swagger.io/v2',
    },
  },
};

Enabling mock generation does not impact the generation of either models or the requests themselves. Also, since we’re using split mode, all mocks will be placed in a .msw.ts file. In our case, running the above configuration with our previously defined API specification will result in the following swaggerPetstore.msw.ts file:

/**
 * Generated by orval v6.19.0 🍺
 * Do not edit manually.
 * Swagger Petstore
 * OpenAPI spec version: 1.0.0
 */
import { faker } from '@faker-js/faker';
import { rest } from 'msw';

export const getAddPetMock = () => ({
  id: faker.number.int({ min: undefined, max: undefined }),
  name: faker.word.sample(),
  tag: faker.helpers.arrayElement([faker.word.sample(), undefined])
});

export const getListPetsMock = () =>
  Array.from({ length: faker.datatype.number({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => ({
    id: faker.number.int({ min: undefined, max: undefined }),
    name: faker.word.sample(),
    tag: faker.helpers.arrayElement([faker.word.sample(), undefined])
  }));

export const getSwaggerPetstoreMSW = () => [
  rest.post('*/pet', (_req, res, ctx) => {
    return res(ctx.delay(1000), ctx.status(200, 'Mocked status'), ctx.json(getAddPetMock()));
  }),
  rest.get('*/pets', (_req, res, ctx) => {
    return res(ctx.delay(1000), ctx.status(200, 'Mocked status'), ctx.json(getListPetsMock()));
  })
];

The generated code includes an msw mock for each request defined in our specification. The values returned by these mocks are generated dynamically using faker and follow the response types and structures defined in our specification. If you want certain mocked values to be static instead, which may simplify assertions during tests, it’s possible to define those under the override property, as follows:

module.exports = defineConfig({
  petstore: {
    input: {
      target: './api.yaml',
    },
    output: {
      workspace: './src/orval/petstore',
      schemas: './model',
      target: './services',
      prettier: true,
      tslint: true,
      mock: true,
      mode: 'split',
      baseUrl: 'https://petstore.swagger.io/v2',
      override: {
        mock: {
          required: true,
          properties: {
            '/tag/': 'test-tag',
            'id': 1,
          },
          arrayMin: 10,
          arrayMax: 10,
          delay: 500
        },
      }
    },
  },
});

Full documentation on the override property can be found here. This property allows the customization of not only mocks but also many other behaviors of Orval. For our scenario we’re setting 5 values inside our mock override:

  • required - sets all optional values as required, so they will always be available on the generated mocks
  • arrayMin and arrayMax - set how many values should be included in arrays
  • delay - customize the msw delay, default is 1000ms
  • properties - the keys to this object can be either the path of a property to be overridden or a regex to match either the property name or path to be overridden. So instead of using faker values, the mocks will contain whatever value you have defined here instead.

Running Orval again with this new configuration yields the following swaggerPetstore.msw.ts file as a result:

/**
 * Generated by orval v6.19.0 🍺
 * Do not edit manually.
 * Swagger Petstore
 * OpenAPI spec version: 1.0.0
 */
import { faker } from '@faker-js/faker';
import { rest } from 'msw';

export const getAddPetMock = () => ({ id: 1, name: faker.word.sample(), tag: 'test-tag' });

export const getListPetsMock = () =>
  Array.from({ length: faker.datatype.number({ min: 10, max: 10 }) }, (_, i) => i + 1).map(() => ({
    id: faker.number.int({ min: undefined, max: undefined }),
    name: faker.word.sample(),
    tag: 'test-tag'
  }));

export const getSwaggerPetstoreMSW = () => [
  rest.post('*/pet', (_req, res, ctx) => {
    return res(ctx.delay(500), ctx.status(200, 'Mocked status'), ctx.json(getAddPetMock()));
  }),
  rest.get('*/pets', (_req, res, ctx) => {
    return res(ctx.delay(500), ctx.status(200, 'Mocked status'), ctx.json(getListPetsMock()));
  })
];

In our newly generated mocks the tag property included in every object has been replaced by our test-tag. This is because we have defined the regex /tag/, so no matter where that property is located, it will be overridden. The id property, on the other hand, has only been replaced in the first mock, since the response is an object that contains an id directly. Meanwhile, the response to the /pets endpoint is an array of objects, so the id properties inside those objects haven’t been overridden. These different approaches allow for a higher degree of customization on mocks.

Conclusion

Orval is a robust NPM library that simplifies data management in web applications. With its code generation, type-safe approach, and a host of features that streamline API data handling, it has become a valuable asset for web developers looking to optimize their workflow, improve code quality, and save valuable time. Whether you're working on a small personal project or a large-scale application, Orval's capabilities make it a must-have in your toolkit. So, if you want to unlock the power of data in your frontend development journey, give Orval a try and experience the difference it can make in your projects.

SHARE THIS STORY

Get. Shit. Done. 👊

Whether your ideas are big or small, we know you want it built yesterday. With decades of experience working at, or with, startups, we know how to get things built fast, without compromising scalability and quality.

Get in touch

Whether your plans are big or small, together, we'll get it done.

Let's get a conversation going. Shoot an email over to projects@betaacid.co, or do things the old fashioned way and fill out the handy dandy form below.

Beta Acid is a software development agency based in New York City and Barcelona.


hi@betaacid.co

About us
locations

New York City

77 Sands St, Brooklyn, NY

Barcelona

C/ Calàbria 149, Entresòl, 1ª, Barcelona, Spain

London

90 York Way, London, UK

© 2025 Beta Acid, Inc. All rights reserved.