Generating a Type-Safe TypeScript SDK with Orval
Learn how to automatically generate a fully type-safe TypeScript SDK from your OpenAPI specification using Orval, eliminating manual API client code and reducing bugs.
When building modern web applications, integrating with REST APIs is inevitable. The traditional approach involves manually writing API client code, creating TypeScript interfaces for request and response types, and maintaining this code as the API evolves. This process is not only time-consuming but also error-prone, often leading to inconsistencies between frontend and backend.
Enter Orval, a code generation tool that automatically creates a fully type-safe TypeScript SDK from your OpenAPI specification. Instead of manually maintaining API client code, you can generate it automatically and catch type errors at compile time rather than runtime.
Throughout this article, I’ll use a real-world example from the DevLille partners-connect project, an event partnership management platform built with Nuxt 4 and TypeScript. This project perfectly demonstrates how Orval can be integrated into a production application.
The Problem with Manual API Client Code
Imagine you’re building a partnership management platform. Your backend exposes endpoints for managing companies, partnerships, events, and sponsoring packages. You need to call these endpoints from your frontend, handle responses, and ensure type safety across your application.
Without Orval, you’d write something like this:
// Manual API client - prone to errors
const response = await axios.get('/api/companies/123');
const company = response.data; // type: any - no type safety!
// Manually created TypeScript interfaces
interface Company {
id: string;
name: string;
email: string;
website?: string;
}
// Manually written API functions
async function getCompany(id: string): Promise<Company> {
const response = await axios.get(`/api/companies/${id}`);
return response.data;
}
This approach has several problems. When your API changes, you must manually update interfaces and functions. There’s no guarantee that your types match the actual API responses. Typos in URLs or parameters only appear at runtime, not during development.
Introducing Orval
Orval solves these problems by generating TypeScript code directly from your OpenAPI specification. The DevLille project uses a Kotlin backend that automatically generates OpenAPI documentation, which is then consumed by Orval to create a type-safe frontend SDK.
Let’s start by installing Orval and Axios in your project:
pnpm add --save-dev orval
pnpm add axios
Setting Up the Configuration
The next step is creating an orval.config.ts file at the root of your project. Here’s the configuration used by the DevLille project:
import { defineConfig } from 'orval';
export default defineConfig({
api: {
input: 'https://your-api.example.com/swagger/documentation.yaml',
output: {
target: './utils/api.ts',
client: 'axios',
override: {
mutator: {
path: './custom-instance.ts',
name: 'customFetch',
},
},
},
},
});
This configuration tells Orval to fetch the OpenAPI specification from a remote URL (in this case, the DevLille backend deployed on Clever Cloud) and generate a single TypeScript file containing all API functions and types. The mutator option allows us to customize the HTTP client with authentication, error handling, and other cross-cutting concerns.
Creating a Custom HTTP Client
Before generating the SDK, we need to create the custom HTTP client that Orval will use. This is where we configure authentication, error handling, and response normalization. Here’s the pattern used in the DevLille project:
import Axios, { AxiosError, AxiosRequestConfig } from 'axios';
export type ApiResponse<T> = {
data: T;
error: boolean;
ok: boolean;
};
export const createCustomAxiosInstance = () => {
const config = useRuntimeConfig();
const instance = Axios.create({
baseURL: config.public.apiBaseUrl || 'http://localhost:8080',
withCredentials: true,
});
instance.interceptors.request.use((config) => {
config.headers['Accept-Language'] = 'fr';
return config;
});
instance.interceptors.response.use(
(response) => ({
...response,
data: {
data: response.data,
error: false,
ok: true,
},
}),
(error: AxiosError) => {
if (error.response?.status === 401) {
// Session expired, redirect to login
window.location.href = '/login';
}
return Promise.resolve({
...error.response,
data: {
data: null,
error: true,
ok: false,
},
});
}
);
return instance;
};
let axiosInstance: ReturnType<typeof Axios.create> | null = null;
export const getAxiosInstance = () => {
if (!axiosInstance) {
axiosInstance = createCustomAxiosInstance();
}
return axiosInstance;
};
export const customFetch = async <T>(
config: AxiosRequestConfig
): Promise<ApiResponse<T>> => {
const instance = getAxiosInstance();
const response = await instance.request<ApiResponse<T>>(config);
return response.data;
};
export default customFetch;
Generating the SDK
Now that everything is configured, we can generate the SDK. Add a script to your package.json:
{
"scripts": {
"generate:api": "orval --config ./orval.config.ts"
}
}
Run the generation command:
pnpm generate:api
Orval will create a single file (in the DevLille project, it’s approximately 111KB containing over 100 API functions). The generated utils/api.ts file includes TypeScript interfaces for all API models like Company, Partnership, and Event, typed functions for every API endpoint, enums for all constants, and complete type safety for all parameters and responses.
Using the Generated SDK
Let’s look at how the DevLille project uses this generated code in practice. Here’s a typical pattern for creating a company and registering a partnership:
import {
getCompanies,
postCompanies,
putCompaniesById,
postEventsPartnership
} from '~/utils/api';
import type {
Company,
CreateCompanySchema,
RegisterPartnershipSchema
} from '~/utils/api';
async function createCompany() {
const newCompany: CreateCompanySchema = {
name: 'Acme Corp',
email: 'contact@acme.com',
website: 'https://acme.com',
};
const response = await postCompanies(newCompany);
if (response.ok) {
console.log('Created company:', response.data);
return response.data;
} else {
console.error('Failed to create company');
return null;
}
}
async function registerPartnership(eventSlug: string, companyId: string, packId: string) {
const partnershipData: RegisterPartnershipSchema = {
companyId,
packId,
selectedOptions: ['booth', 'logo-website', 'social-media'],
};
const response = await postEventsPartnership(eventSlug, partnershipData);
if (response.ok) {
const partnership = response.data;
console.log('Partnership registered with ID:', partnership.id);
return partnership;
}
return null;
}
Notice how TypeScript knows the exact shape of every request and response. If you misspell a property or pass the wrong type, your IDE will immediately flag it. When the backend API changes, regenerate the SDK and TypeScript will show you exactly what broke.
Automating SDK Generation in CI/CD
To ensure your frontend stays synchronized with your API, automate SDK generation in your CI/CD pipeline. Here’s a GitHub Actions workflow that regenerates the SDK and validates that it’s up to date:
name: Generate API SDK
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
generate-sdk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate API SDK
run: pnpm generate:api
- name: Check for changes
run: |
if [[ -n $(git status --porcelain) ]]; then
echo "Generated files have changed. Please run 'pnpm generate:api' locally."
git diff
exit 1
fi
- name: Run TypeScript check
run: pnpm type-check
This workflow ensures that any API changes are immediately reflected in the generated SDK, and TypeScript type checking catches breaking changes before they reach production.
Best Practices and Lessons Learned
Working with Orval on a real project like DevLille partners-connect reveals several important practices. Always regenerate your SDK after API changes by running pnpm generate:api, and consider adding this to your pre-commit hooks or CI pipeline. While some teams prefer not to commit generated code, committing Orval-generated files makes code reviews easier since you can see exactly what changed in the API.
Store your OpenAPI specification in version control or fetch it from a versioned endpoint. The DevLille project fetches their spec from a deployed backend, ensuring the frontend always stays synchronized with the deployed API. Run TypeScript’s type checker immediately after regenerating to catch breaking changes: pnpm generate:api && pnpm type-check.
Conclusion
Orval transforms how you build TypeScript applications that consume REST APIs. By automatically generating a fully type-safe SDK from your OpenAPI specification, you eliminate manual API client code, catch inconsistencies at compile time, stay synchronized with backend changes automatically, and reduce bugs from typos or outdated types.
The DevLille partners-connect project demonstrates these benefits in a real production environment. With over 100 generated API functions and complete type safety across their partnership management platform, they’ve eliminated an entire class of bugs and improved their development workflow significantly.
Whether you’re building a Vue application like DevLille or working with React, Angular, or any other TypeScript framework, Orval adapts to your needs. The initial setup takes minutes, but the long-term benefits make it an essential tool for any TypeScript project consuming REST APIs.
You can explore the complete DevLille partners-connect project on GitHub to see these patterns in action across a full production application.