API Client Generation (Orval)
Relevant source files
The following files were used as context for generating this wiki page:
The Collimator frontend utilizes Orval to automatically generate a type-safe TypeScript API client. This client is derived from the OpenAPI specification exposed by the NestJS backend. This process ensures that the frontend's data models and API-calling functions are always synchronized with the backend API, significantly reducing boilerplate code and preventing type-related bugs.
The generation process is initiated via a script, which fetches the API specification, and uses the Orval CLI to produce a set of files including data models (DTOs), endpoint functions, and mocks.
Architecture Overview
The API client generation process involves both the backend and frontend projects. The backend is responsible for generating an up-to-date OpenAPI specification, and the frontend consumes this specification to generate the client code.
This diagram illustrates the end-to-end flow of API client generation and usage.
Backend: OpenAPI Specification
The NestJS backend uses the @nestjs/swagger package to generate an OpenAPI v3 document. This is configured in backend/src/swagger.ts.
- A
DocumentBuilderis used to set the title, description, and security requirements for the API. - The application creates and serves the OpenAPI specification as a JSON file, which is made available at the
/api-jsonendpoint during development. This endpoint is the direct input source for Orval.
// backend/src/swagger.ts:19-26
const config = new DocumentBuilder()
.setTitle("Collimator")
.setDescription(`The Collimator API description (${v})`)
.setVersion(version)
.addBearerAuth()
.addSecurityRequirements("bearer")
.build();
Sources: backend/src/swagger.ts:5-27
Frontend: Orval Configuration
The behavior of the Orval CLI is defined in frontend/orval.config.ts. This file specifies the input source for the API spec and dictates how the output files should be structured.
// frontend/orval.config.ts
import { defineConfig } from "orval";
const config = defineConfig({
collimator: {
input: {
target: "http://localhost:3001/api-json",
validation: false,
},
output: {
mode: "tags-split",
target: "src/api/collimator/generated/endpoints",
schemas: "src/api/collimator/generated/models",
client: "fetch",
httpClient: "fetch",
mock: true,
override: {
fetch: {
includeHttpResponseReturnType: false,
},
mutator: {
path: "./src/api/fetch.ts",
name: "fetchApi",
},
},
},
hooks: {
afterAllFilesWrite: {
command: "yarn lint --fix",
injectGeneratedDirsAndFiles: false,
},
},
},
});
export default config;
Sources: frontend/orval.config.ts
Key Configuration Options
| Option | Value | Description |
|---|---|---|
input.target | http://localhost:3001/api-json | The URL of the OpenAPI specification JSON file. |
output.mode | tags-split | Generates separate files for each API tag (e.g., classes, tasks). |
output.target | src/api/collimator/generated/endpoints | The directory where endpoint functions are saved. |
output.schemas | src/api/collimator/generated/models | The directory where TypeScript interfaces for DTOs are saved. |
output.client | fetch | Specifies that the generated client should use the fetch API. |
output.mock | true | Instructs Orval to generate mock data and MSW handlers for testing. |
override.mutator.name | fetchApi | Specifies a custom fetch wrapper function to be used for all API calls. |
hooks.afterAllFilesWrite | yarn lint --fix | A command that is executed after all files are generated to ensure code style consistency. |
Sources: frontend/orval.config.ts:6-29
Generation Process
The API client generation is handled by a single script in frontend/package.json.
// frontend/package.json:11
"update:api": "shx rm -rf src/api/collimator/generated && orval",
Running yarn update:api performs the following steps:
- Deletes the existing
src/api/collimator/generateddirectory to ensure a clean build. - Executes the
orvalcommand, which reads the configuration fromorval.config.tsand generates the new client files.
This flow is visualized below.
Generated Client Code
The output of the Orval generation process is a set of TypeScript files that are used throughout the frontend application.
Models (DTOs)
For each data schema in the OpenAPI specification, Orval generates a corresponding TypeScript interface. This provides strong typing for API payloads and responses.
Example of a generated model for creating a task:
// frontend/src/api/collimator/generated/models/createTaskDto.ts
/**
* Generated by orval v7.6.0 🍺
* Do not edit manually.
* Collimator
* The Collimator API description (multi-version)
* OpenAPI spec version: 1.0.0
*/
import type { TaskType } from "./taskType";
import type { CreateReferenceSolutionDto } from "./createReferenceSolutionDto";
export interface CreateTaskDto {
title: string;
description: string;
/** The task's type. */
type: TaskType;
/** Task file */
taskFile: Blob;
/** Reference solution files */
referenceSolutionsFiles: Blob[];
/** The reference solutions for this task. */
referenceSolutions: CreateReferenceSolutionDto[];
}
Sources: frontend/src/api/collimator/generated/models/createTaskDto.ts
These generated types are used across the application, including in E2E tests to ensure correct data structures.
// e2e/tests/users/user-management.spec.ts:5
import { UserType } from "@/api/collimator/generated/models";
Sources: e2e/tests/users/user-management.spec.ts:5
Custom Hooks and Usage
While Orval generates the base API-calling functions, the project builds custom React Hooks on top of them for data fetching, typically using swr. This encapsulates data-fetching logic and provides a clean, reusable interface for components.
The following diagram shows the sequence of a component fetching data using a custom hook that wraps a generated Orval function.
An example is the SessionForm component, which uses the useAllTasks hook to fetch a list of available tasks to be added to a session.
// frontend/src/components/session/SessionForm.tsx:37
const { isLoading, data, error } = useAllTasks();
Sources: frontend/src/components/session/SessionForm.tsx:37
Conclusion
The use of Orval for API client generation is a cornerstone of the frontend architecture in Collimator. It establishes a robust, type-safe contract between the frontend and backend, automates the tedious process of writing API-layer code, and allows developers to focus on building features with confidence that the data layer is consistent and correct. The combination of backend-driven spec generation, Orval's powerful configuration, and custom React hooks creates a highly efficient and maintainable development workflow.