API Code Generation #
This document describes how we generate type-safe API clients in the webapp using Orval, and how to use the generated code with React Query to make API calls.
Overview #
We use Orval to generate TypeScript clients from our OpenAPI specification. This provides several benefits:
- Type Safety: Full TypeScript types for requests and responses
- DRY: No need to manually maintain API client code
- Developer Experience: Autocomplete and type checking for API calls
- Integration with React Query: Generated hooks for data fetching with all the benefits of React Query
Run the code generation #
mise run codegen
The codegen will run automatically when you run mise dev or mise ci.
Setup #
The Orval configuration is located in apps/webapp/orval.config.ts. This configuration specifies:
import { defineConfig } from "orval";
export default defineConfig({
bonsapi: {
input: "../bonsapi/docs/openapi/index.yaml",
output: {
mode: "tags-split",
clean: true,
target: "./src/shared/lib/api/_generated/",
client: "react-query",
mock: true,
override: {
mutator: {
path: "./src/shared/lib/api/axios-instance.ts",
name: "axiosInstance",
},
},
},
hooks: {
afterAllFilesWrite: "pnpm webapp format:fix",
},
},
});
Key points in this configuration:
- OpenAPI source from the BonsAPI project
- Output organized by API tags for better organization
- React Query integration for hooks
- MSW mock generation for testing
- Custom Axios instance for API requests
- Auto-formatting after generation
Generated Files #
When you run the code generation process, Orval generates several files:
src/shared/lib/api/_generated/bonsAPI.schemas.ts: TypeScript type definitions for all API schemassrc/shared/lib/api/_generated/document/document.ts: API client for document-related endpointssrc/shared/lib/api/_generated/default/default.ts: API client for other endpoints- MSW handlers for mocking in tests and development (e.g.,
document.msw.ts)
Axios Configuration #
The Axios instance used by the generated clients is configured in src/shared/lib/api/axios-instance.ts:
import type { AxiosError, AxiosRequestConfig } from "axios";
import Axios from "axios";
const NODE_ENV = process.env.NODE_ENV;
const BASE_URL =
NODE_ENV === "development"
? `http://${process.env.NEXT_PUBLIC_BONSAPI_HOST}`
: `https://${process.env.NEXT_PUBLIC_BONSAPI_HOST}`;
export const AXIOS_INSTANCE = Axios.create({ baseURL: BASE_URL });
export const axiosInstance = <T>(
config: AxiosRequestConfig,
options?: AxiosRequestConfig
): Promise<T> => {
const source = Axios.CancelToken.source();
const promise = AXIOS_INSTANCE({
...config,
...options,
cancelToken: source.token,
}).then(({ data }) => data as T);
// @ts-expect-error - cancel is a method on the promise
promise.cancel = () => {
source.cancel("Query was cancelled");
};
return promise;
};
export type ErrorType<e> = AxiosError<e>;
export type BodyType<BodyData> = BodyData;
This configuration automatically uses the appropriate base URL based on the environment and provides cancellation support for requests.
How to Use the Generated API Clients #
Basic Usage with React Query #
To make an API call, import the generated hook from the appropriate file. Here’s an example of fetching documents:
import { useGetDocuments } from "@/shared/lib/api/_generated/document/document";
function DocumentList() {
const { data: documents, isLoading, error } = useGetDocuments();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{documents?.map((doc) => (
<div key={doc.id}>{doc.title}</div>
))}
</div>
);
}
Making Requests with Parameters #
For endpoints that require parameters, you can pass them to the hook:
import { useGetDocumentById } from "@/shared/lib/api/_generated/document/document";
function DocumentDetail({ id }) {
const { data: document, isLoading } = useGetDocumentById({
pathParams: { id },
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>{document?.title}</h1>
<p>{document?.content}</p>
</div>
);
}
Creating or Updating Resources #
For mutations (POST, PUT, PATCH), the generated hooks return mutation functions from React Query:
import { useCreateDocument } from "@/shared/lib/api/_generated/document/document";
function DocumentForm() {
const createDocument = useCreateDocument();
const handleSubmit = async (formData) => {
try {
// Note the structure with 'data' property as shown in the generated code
const result = await createDocument.mutateAsync({
data: {
files: formData.files,
entity_id: formData.entityId,
},
});
console.log("Document created:", result);
} catch (error) {
console.error("Failed to create document:", error);
}
};
return <form onSubmit={handleSubmit}>{/* Form fields */}</form>;
}
Working with File Uploads #
For endpoints that handle file uploads (like document creation), the generated code automatically handles FormData conversion:
import { useCreateDocument } from "@/shared/lib/api/_generated/document/document";
function FileUploadForm() {
const [files, setFiles] = useState<File[]>([]);
const createDocument = useCreateDocument();
const handleFileChange = (e) => {
setFiles(Array.from(e.target.files));
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const result = await createDocument.mutateAsync({
data: {
files,
entity_id: "invoice-123",
},
});
// Handle success
} catch (error) {
// Handle error
}
};
return (
<form onSubmit={handleSubmit}>
<input type="file" multiple onChange={handleFileChange} />
<button type="submit">Upload</button>
</form>
);
}
Query Options #
You can pass React Query options to customize behavior:
const { data } = useGetDocuments({
query: {
enabled: isAuthenticated,
staleTime: 5 * 60 * 1000, // 5 minutes
refetchOnWindowFocus: false,
},
});
Regenerating API Clients #
When the API schema changes, you need to regenerate the clients:
cd apps/webapp
pnpm orval
This will fetch the latest OpenAPI spec and regenerate all the TypeScript files.
Best Practices #
- Use type inference: Let TypeScript infer the types from the generated clients
- Handle loading and error states: Always account for loading, error, and success states
- Use React Query’s caching: Take advantage of the built-in caching features
- Invalidate queries properly: When data changes, invalidate relevant queries
// Example of invalidating queries
import { useQueryClient } from "@tanstack/react-query";
import {
useCreateDocument,
getGetDocumentQueryKey,
} from "@/shared/lib/api/_generated/document/document";
function DocumentForm() {
const queryClient = useQueryClient();
const createDocument = useCreateDocument();
const handleSubmit = async (formData) => {
try {
await createDocument.mutateAsync({
data: {
/* document data */
},
});
// Invalidate relevant queries to refresh the data
queryClient.invalidateQueries(getGetDocumentQueryKey());
} catch (error) {
// Handle error
}
};
}
Troubleshooting #
If you encounter issues with the generated code:
- Check that you’re using the latest OpenAPI spec
- Verify the Orval configuration is correct
- Clear the generated files and regenerate them
- Ensure your API calls match the expected parameters in the spec
- Check for console errors related to the Axios requests