
Orval - Generate more from your Open API Doc
Generating Typescript interfaces from an Open API doc is old news - but did you know you can also generate HTTP request code and even mock data using Orval? With support for Fetch, Axios, React Query, Angular, and more, it probably supports your HTTP client. It can even generate MCP server code!
What is Orval?
Orval is a command line utility that can generate Typescript interfaces/types, HTTP request code, and even mock data with MSW.
If you're writing typescript code and issuing requests to Open API compliant REST endpoints in another language or repository, Orval is the tool you didn't know you needed.
How do you use it?
It is driven by the orval.config.ts file, typically placed in the root of your project. In the Orval config file, you define where Orval should look for your Open API document (relative file path or URL), where and how it should organize the generated code, specify your client, and you're off to the races.
Orval supports:
- Fetch API
- Axios
- (Tanstack) React Query
- Angular (the built-in HttpClient)
- Svelte Query
- Vue Query
- SWR
- Hono
- Custom HTTP Client
- Zod schema generation
- MCP Server generation
What exactly does Orval generate for you you?
Orval will generate:
- Schema (Typescript interfaces )
- Typescript interfaces generated from API models
- Typescript object constants will be generated for any enums in API models or returned by an API
- HTTP request code (based on your client)
- In Angular (as used in example project), it will use the built-in
HttpClientand will generate the Angular service file with methods for each endpoint. - If you're using Tanstack Query (formerly React query), for example, you can specify that in your config and it will generate the custom hooks, the queries/mutations, etc (using Axios by default).
- In Angular (as used in example project), it will use the built-in
- Mock data (using MWS + faker.js)
Sample - generated schema
Below is a excerpt from the generated todoApi.schemas.ts file from the example repository.
/**
* Generated by orval v7.11.2 🍺
* Do not edit manually.
* TodoApi
* OpenAPI spec version: v1
*/
export interface ActionLogResponseDto {
id?: number;
/** @nullable */
username?: string | null;
timestamp?: string;
/** @nullable */
action?: string | null;
}
export interface AuthResponseDto {
/** @nullable */
token?: string | null;
/** @nullable */
username?: string | null;
/** @nullable */
email?: string | null;
expires?: string;
}
export interface ChangePasswordDto {
/** @minLength 1 */
currentPassword: string;
/**
* @minLength 6
* @maxLength 100
*/
newPassword: string;
/** @minLength 1 */
confirmNewPassword: string;
}
export interface CreateTodoDto {
/**
* @minLength 1
* @maxLength 500
*/
text: string;
}
// Truncated for brevity
Sample - generated HTTP code (Angular Service)
This example project uses angular so Orval generates the request code using Angular's built-in HTTP client and service file pattern.
Below is an excerpt from the generated todos.service.ts file from the example repository.
interface HttpClientOptions {
headers?: HttpHeaders | Record<string, string | string[]>;
context?: HttpContext;
params?:
| HttpParams
| Record<
string,
string | number | boolean | ReadonlyArray<string | number | boolean>
>;
reportProgress?: boolean;
withCredentials?: boolean;
credentials?: RequestCredentials;
keepalive?: boolean;
priority?: RequestPriority;
cache?: RequestCache;
mode?: RequestMode;
redirect?: RequestRedirect;
referrer?: string;
integrity?: string;
transferCache?: { includeHeaders?: string[] } | boolean;
timeout?: number;
}
@Injectable({ providedIn: 'root' })
export class TodosService {
private readonly http = inject(HttpClient);
getTodos<TData = TodoResponseDto[]>(
options?: HttpClientOptions & { observe?: 'body' },
): Observable<TData>;
getTodos<TData = TodoResponseDto[]>(
options?: HttpClientOptions & { observe: 'events' },
): Observable<HttpEvent<TData>>;
getTodos<TData = TodoResponseDto[]>(
options?: HttpClientOptions & { observe: 'response' },
): Observable<AngularHttpResponse<TData>>;
getTodos<TData = TodoResponseDto[]>(
options?: HttpClientOptions & { observe?: any },
): Observable<any> {
return this.http.get<TData>(`/api/Todos`, options);
}
// truncated for brevity
Samples - Generated Mock Data/requests
Gives you utility functions to generate mock data as well as to execute mock requests. Below excerpts can be found in todos.msw.ts in the example repository
Function returning mock data
export const getCreateTodoResponseMock = (
overrideResponse: Partial<TodoResponseDto> = {},
): TodoResponseDto => ({
id: faker.helpers.arrayElement([
faker.number.int({ min: undefined, max: undefined, multipleOf: undefined }),
undefined,
]),
text: faker.helpers.arrayElement([
faker.helpers.arrayElement([
faker.string.alpha({ length: { min: 10, max: 20 } }),
null,
]),
undefined,
]),
status: faker.helpers.arrayElement([
faker.helpers.arrayElement([0, 1, 2] as const),
undefined,
]),
createdAt: faker.helpers.arrayElement([
`${faker.date.past().toISOString().split('.')[0]}Z`,
undefined,
]),
updatedAt: faker.helpers.arrayElement([
`${faker.date.past().toISOString().split('.')[0]}Z`,
undefined,
]),
completedAt: faker.helpers.arrayElement([
faker.helpers.arrayElement([
`${faker.date.past().toISOString().split('.')[0]}Z`,
null,
]),
undefined,
]),
...overrideResponse,
});
Function returning mock request
export const getCreateTodoMockHandler = (
overrideResponse?:
| TodoResponseDto
| ((
info: Parameters<Parameters<typeof http.post>[1]>[0],
) => Promise<TodoResponseDto> | TodoResponseDto),
) => {
return http.post('*/api/Todos', async (info) => {
await delay(1000);
return new HttpResponse(
JSON.stringify(
overrideResponse !== undefined
? typeof overrideResponse === 'function'
? await overrideResponse(info)
: overrideResponse
: getCreateTodoResponseMock(),
),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
);
});
};
Orval Setup and Configuration
Once you have it setup, generating types, HTTP request code, and mocks are as easy as running an NPM script.
Installation
npm i orval -D
npm i msw axios @faker-js/faker
Add the following to the scripts object in your package.json file:
"generate:api": "orval --config orval.config.ts"
Orval Configuration
Example Config File
// orval.config.ts
import { faker } from '@faker-js/faker';
import { defineConfig } from 'orval';
export default defineConfig({
todoAPI: {
input: {
target: 'http://localhost:5000/swagger/v1/swagger.json',
parserOptions: {
resolve: {
http: {
canRead: /http:/,
},
},
}
},
output: {
mode: 'tags-split',
target: 'src/app/__generated__/todoAPI',
schemas: 'src/app/__generated__/todoAPI/model',
client: 'angular',
mock: true,
override: {
operations: {
GetTodos: {
mock: {
data: () => {
return Array.from({ length: 10 }, () => ({
id: faker.number.int({ min: 1, max: 100 }),
text: faker.lorem.sentence(),
createdAt: faker.date.past(),
status: faker.number.int({ min: 0, max: 2 }),
completedAt: undefined,
}));
},
},
},
},
},
},
hooks: {
afterAllFilesWrite: 'prettier --write',
},
},
});
Input Settings
The target property on the input config should point to your Open API document. It can be file path or a URL. See Docs
In the example above, the target is set to the URL at which my local API server is serving up my Open API document.
parserOptions
While the documentation says this is optional, I have to include it here because the localhost URL I'm pointing Orval to for my Open API document doesn't have a security certificate. The parserOptions allows me to tell Orval that it's okay.
But what if accessing your Open API document requires an API key or is protected by authentication? No problem. parserOptions allows you to specify domains, headers, etc. See Docs
Output Settings
client
This determines what kind of code gets generated. As mentioned before, Orval supports a ton of clients. See Docs
In my example, the client is set to angular. Orval generates the angular service and the request methods using Angular's built-in HttpClient.
If you set this to react-query, for example, it would generate the request functions, react query implementation complete with query keys and custom hooks for your requests.
httpClient
This field allows you to specify the http client you want to use. By default, Orval uses axios, but you can change this field to use native fetch. See Docs
target
The path to the file or directory which will contain the generated code. As seen in the sample configuration, I like to set this to a directory (src/app/__generated__/todoAPI). I also like to put all of this inside of a __generated__ directory so that any other people working on the project knows not to change anything in there because it will be overwritten when code is generated again. See Docs
mode
This property gives you control over how Orval generates the code. According to the docs, you have a 4 options:
tags
- This splits the generated code into files corresponding to the
tagsproperties from your OpenAPI document. All code (requests, models, classes, mocks, etc.) generated would be placed inside of the file corresponding to the tag. Example output:
└── __generated__/
└── todoAPI/
├── action-logs.ts
├── auth.ts
├── todoApi.schemas.ts
└── todo.ts
tags-split(Recommended)
- This option creates a sub-directory for each tag from your OpenAPI document. Generated code for each tag is split into files and placed in the corresponding sub-directory. The generated types (schemas) for all tags are placed in a single file.
- Note: I am generating mocks so there will be a msw.ts file generated for me containing the code to handle data mocking
└── __generated__/
└── todoAPI/
├── action-logs/
│ ├── action-logs.ts
│ └── action-logs.msw.ts
├── auth/
│ ├── auth.ts
│ └── auth.msw.ts
├── todo/
│ ├── todo.ts
│ └── todo.msw.ts
└── todoApi.schemas.ts
split
- This option splits generated code based on type (mocks, schemas, request functions, etc) into separate files.
└── __generated__/
└── todoAPI/
├── todoApi.msw.ts
├── todoApi.schemas.ts
└── todoApi.service.ts
single
- This option generates all code into a single file.
└── __generated__/
└── todoAPI/
└── todoApi.ts
namingConvention
Allows you to specify your preference for the way your files/directories are named. This only applies to directories and the files containing non-schema code. Your schema (models/types) will still be camelCase.
In my example, I use kebab-case because that is conventional with Angular. But other possible values include camelCase, PascalCase, and snake_case. See Docs
mock
The default value is false, but when true Orval will generate mock requests and mock data. By default, it uses MSW for mock requests and faker for mock data. Orval also supports Cypress in place of MSW. See Docs.
useExamples
Uses example/examples fields from Open API document to generate mock data instead of faker.
docs
Allows the user to configure and generate API documentation using TypeDoc in a markdown format. This is a super powerful option for creating a documentation site.
override
Allows you to override the output like your mock implementation or transform the API implementation like you want. For more information, see docs.
For an explanation/example of using this field to override mock data, see Overriding mock data.
Orval hooks
Allows you to run scripts. At the time this article was written, the only hook available is afterAllFilesWrite. One thing we can do with that is run prettier to format generated code according to the standards you have setup in your project
hooks: {
afterAllFilesWrite: 'prettier --write',
},
Multiple API Projects? No Problem.
Looking at the configuration file, you can see that the structure of it allows for multiple API projects. This means that you can have a configuration object per API (or per Open API document). In this project example, notice all of my Todo API configuration is inside of a todoAPI object.
Want to regenerate types only for one of many API projects you have configured? No problem. The Orval CLI allows you to specify which project you want to target.
Example:
orval --project todoAPI
Overriding Code Generation Defaults
For example, when I was working on this example project, the function returning mock todos would sometimes have values that did not represent the data I expected. That's because it used faker to generate strings based on the data type, which is a sensible default. But let's say I wanted to todo text to always be a sentence. We can override that in the config file to be a bit more specific where we need to be.
Below, I will show how I am modifying the mock data for the GetTodos endpoint.
Note: To do this, the API endpoint you want to target needs to have a unique Operation ID in your OpenAPI document. You reference this Operation ID in the Orval config in order to override the code generated based on the target endpoint.
In this project example, we'll zoom in on this section of the Orval config:
output: {
...
override: {
operations: {
GetTodos: {
mock: {
data: () => {
return Array.from({ length: 10 }, () => ({
id: faker.number.int({ min: 1, max: 100 }),
text: faker.lorem.sentence(),
createdAt: faker.date.past(),
status: faker.number.int({ min: 0, max: 2 }),
completedAt: undefined,
}));
},
},
},
},
},
}
This override, always returns an array of 10 To-Do items with faker generated data. You can write your own custom function to return whatever makes sense for your use case.
Limitations
Be mindful that the output can be only as good as the input. The Open API spec is pretty loose so if you're not careful, you may not have enough information in your Open API document for Orval to generate everything you need or in the way that you need it. Let's look at an example:
Enums may result in unusable TS constants
In my sample project, I had a TodoStatus enum. When I first used Orval to generate the types, it produced this Typescript constant:
export const TodoStatus = {
NUMBER_0: 0,
NUMBER_1: 1,
NUMBER_2: 2,
} as const;
Obviously, that isn't ideal. Next, I took a look at my Open API document and this is what it had for TodoStatus:
"TodoStatus": {
"enum": [
0,
1,
2
],
"type": "integer",
"format": "int32"
}
The Open API document didn't include any information about what these enums mean so Orval didn't generate a more appropriately named constant because the Open API spec didn't provide enough information. But why? After all, In C#, my model looked fine:
public enum TodoStatus
{
Incomplete,
InProgress,
Complete
}
It turns out, I had to include some attributes to help .NET include more information about this enum in the Open API doc. So my model ended up looking like:
[JsonConverter(typeof(JsonStringEnumConverter<TodoStatus>))]
public enum TodoStatus
{
Incomplete = 0,
InProgress = 1,
Complete = 2
}
This produced the following for TodoStatus in my Open API document:
"TodoStatus": {
"enum": [
"Incomplete",
"InProgress",
"Complete"
],
"type": "string"
},
Now, Orval can generate a much more usable/readable constant:
export const TodoStatus = {
Incomplete: 'Incomplete',
InProgress: 'InProgress',
Complete: 'Complete',
} as const;
Note: You might notice that there's a difference here. Before, we were sending and receiving an integer for TodoStatus before. Now, we're sending and receiving string values . Well, in .NET the JsonStringEnumConverter not only helps improve the Open API document, but it also handles the conversion of the string to and from the integer. So if we look in the database, you will still see an integer in the TodoStatus column on the Todos table.
Issues (Explanation + work-around)
Output is only as good as the input
As mentioned above, Orval can only generate code based on the information it has in the Open API document. If the Open API document is missing information or is ambiguous, the generated code may not be what you expect or need.
Generated Composite Types not imported in MSW files
Orval + MSW did a great job generating the function that returns mocks. But in some files, the generated composite types were used, but not imported. Which led to errors like this:

This happened because the OpenAPI document generated from my .NET endpoints specified a content-type of plain/text. If this happens to you, you will need to figure out what is needed for your OpenAPI document generator to specify that the content-type of your endpoints is application/json.
Orval + MSW knew that the generated mocks should have the composite type, but didn't import it. This is a bug with Orval - if it knew to assign the composite return type, it could have known to add the import statement.
A Github issue was filed for this here, but was closed due to the work around I mentioned above.
For my .NET project, I solved this by removing the StringOutputFormatter from my controller configuration.
// program.cs
builder.Services.AddControllers(options =>
{
options.OutputFormatters.RemoveType<StringOutputFormatter>();
});
Example Project
Let's take a look at the setup by diving into an example. You can find the example project described below here.
The Front-end (Angular v21)
- NgRx Signal Store for state management
- Angular Material for UI components
The Back-end (.NET 9 Web API + SQLite)
I find it best to look at an example we're all familiar with - the classic To-do App.
The Back-end is a simple .NET 9 web API project with a SQLite database. It includes:
- Simple JWT-based authentication/authorization
- Including endpoints to sign up, login, reset password, etc.
- SQLite for simple data persistence
- Protected endpoints for creating, reading, updating, and deleting to-do items
- Swashbuckle to generate Swagger UI for testing endpoints
Swagger UI
Below is a screenshot of the Swagger UI running on localhost for this project so you can see what endpoints we're working with.
