A Brief Introduction to OpenLayers and Web Mapping
- Development
Guilherme Mierzwa Guilherme Mierzwa
October 25, 2023 • 23 min read
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.
There are 2 basic requirements to start using Orval in your project:
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.
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.
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 folderschemas - separates types and interfaces into their own files on a different folderprettier - runs prettier on the generated code, in order to keep it consistent with the rest of your applicationtslint - runs tslint on the generated codeThis 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.
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:
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.@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.paramsSerializer. With this it’s possible to change the format of arrays and other query params to meet the API’s requirements.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.
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 mocksarrayMin and arrayMax - set how many values should be included in arraysdelay - customize the msw delay, default is 1000msproperties - 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.
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.