Generate TypeScript SDKs and React Query Hooks for NextJS with Orval and OpenAPI Specs - Andy Primawan

Tired of manually writing TypeScript interfaces, API clients, and React Query hooks? Discover how Orval can automate this entire process by generating a type-safe client SDK directly from your OpenAPI specification. Boost your productivity and eliminate boilerplate code in your Next.js projects.

Generate TypeScript SDKs and React Query Hooks for NextJS with Orval and OpenAPI Specs - Andy Primawan
Generate TypeScript SDK and React Query hooks with Orval and OpenAPI spec - Andy Primawan

Introduction

As full-stack engineers, we're constantly building bridges between the frontend and the backend. On a modern stack like Next.js, TypeScript, and a RESTful backend, a crucial part of this bridge is the API client layer. This layer is responsible for fetching data, handling state, and ensuring type safety.

The backend often provides an OpenAPI (formerly Swagger) specification, a fantastic contract that defines every endpoint, model, and operation. The challenge, however, is translating this contract into robust, type-safe, and maintainable frontend code. This is where the manual work usually begins, but it doesn't have to.

The Problem: The Manual Treadmill

If you've worked on a project with a rapidly evolving API, you'll recognize this painful cycle:

  1. Manual Type Definitions: A backend developer adds a new field to a User model. You now have to hunt down the User interface in your frontend codebase and manually add that field. Miss it, and you've introduced a potential runtime bug.
  2. Boilerplate API Layer: For every single endpoint, you write a fetch or axios function. You handle the URL, method, headers, request body, and response parsing. This is repetitive and error-prone. Did you remember to add the Content-Type header for that new POST request?
  3. Hand-Crafting React Query Hooks: On top of your fetchers, you write useQuery and useMutation hooks. You need to manage query keys, define mutation functions, and handle invalidation logic to keep your cache fresh. This adds yet another layer of boilerplate that needs to be consistent across the entire application.

This manual process is not just tedious; it's fragile. It creates a disconnect between the API's "single source of truth" (the OpenAPI spec) and the client implementation. When the API changes, your client code becomes stale, leading to bugs, wasted time, and developer frustration.

The Solution: Generate, Don't Hand-Craft

What if we could treat our API client code as a build artifact, just like compiled code? What if we could generate everything—interfaces, fetchers, and React Query hooks—directly from the OpenAPI spec?

Enter Orval.

Orval is a powerful code generator that consumes an OpenAPI specification and outputs a fully-typed TypeScript client. It's incredibly flexible and, most importantly for us, has first-class support for generating React Query hooks.

Here’s how to set it up in your Next.js project.

Step 1: Installation

First, add Orval and its React Query preset to your project's dev dependencies.

npm i orval -D
# or
yarn add orval -D
# or
pnpm add orval -D

Step 2: Configuration

Create a configuration file named orval.config.js in your project's root. This file tells Orval where to find your API spec, where to put the generated code, and how to generate it.

// orval.config.js

module.exports = {
    dojotekchatbot: {
      output: {
        mode: 'tags-split',
        target: 'src/sdk/dojotekchatbot.ts',
        schemas: './src/sdk/models',
        client: 'react-query',
        mock: true,
        httpClient: 'axios',
        baseUrl: 'http://localhost:3000',
      },
      input: {
        target: './dojotek-chatbot.yaml',
      },
    },
  };
  

The configuration above tells Orval to:

  1. Create a dedicated folder for each OpenAPI tag.
  2. Generate OpenAPI schemas (data model) into src/sdk/models/ folder.
  3. Use the react-query client generator.
  4. Generate MSW mocks.
  5. Using axios as the HTTP client rather than fetch.
  6. Base URL of the live backend is http://localhost:3000
    (change to real life URL for production, for example https://ai-chatbot-be.dojotek.net
  7. Read the OpenAPI spec from a local file dojotek-chatbot.yaml.
  8. Generate output files at folder src/sdk.

Here sample folder structure of the generated TypeScript SDK files, we have the model folder contains OpenAPI schemas, and each NestJS controller splitted to its own folder (OpenAPI tags).

Sample of TypeScript SDK files generated by Orval from OpenAPI spec file - Andy Primawan

And here sample usage of React Query hooks generated by Orval, in a ReactJS component to be shown on a NextJS route page:

Sample usage of React Query hooks generated by Orval for NextJS - Andy Primawan

Step 3: Generating the SDK

Add a script to your package.json to run Orval.

// package.json
{
  "scripts": {
    "dev": "next dev --turbopack --port 4000",
    "build": "npm run generate:api && next build",
    "start": "next start --port 4000",
    "lint": "eslint",
    "generate:api": "orval --config ./orval.config.js"
  }
}

Now, simply run the command:

npm run generate:api
# or
yarn generate:api
# or
pnpm generate:api

Orval will read your API spec and generate src/sdk/, which will contain:

  • TypeScript interface or type for every data model in your API.
  • A custom React Query hook (useQuery, useInfiniteQuery, useMutation) for every single endpoint.

Step 4: Don't Commit Generated Code

This is a critical mindset shift. The generated src/sdk/ folder is a build artifact, not source code. It should be regenerated, not manually edited. Therefore, you should add it to your .gitignore file.

# .gitignore

# ...other ignores

# Generated API client
/src/sdk/

By adding npm run generate:api to your build script, you ensure that your project always builds against the latest API contract, both locally and in your CI/CD pipeline. For development, you just need to re-run npm run generate:api whenever the backend API changes.

Step 5: Using the Hooks

Now for the best part. Using your API in your React components becomes incredibly clean and simple.

Instead of this...

// The old way
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

interface ChatAgent {
  id: number;
  name: string;
  tag?: string;
}

const fetchChatAgents = async (): Promise<ChatAgent[]> => {
  const { data } = await axios.get('/chat-agents');
  return data;
};

function ChatAgentList() {
  const { data, isLoading } = useQuery<ChatAgent[]>(['chat-agents'], fetchChatAgents);
  // ...
}

...you now do this:

// The Orval way
import { useChatAgentsControllerFindAll } from "@/sdk/chat-agents/chat-agents"; // Just import the hook!

function ChatAgentList() {
  const { data, isLoading } = useChatAgentsControllerFindAll();

  if (isLoading) return <div>Loading...</div>;

  return (
    <ul>
      {data?.data.map((chatAgent) => (
        <li key={chatAgent.id}>{chatAgent.name}</li>
      ))}
    </ul>
  );
}

Look at that! No manual types, no fetcher function, no useQuery boilerplate. You get full type safety on the response data, query parameters, and request bodies for mutations, all for free.

Step 6: Handling Authorization with JWT

It's rare for an API to be completely public. Most of the time, you'll be interacting with protected endpoints that require an authentication token, commonly a JSON Web Token (JWT), sent as a bearer token in the Authorization header.

Manually adding this header to every single API call is not just tedious, it's a security risk if you forget it. This is where the power of using a custom axios instance with interceptors shines.

For example we can create src/lib/axios.ts to add interceptor:

// src/lib/axios.ts
"use client";

import axios from "axios";
import { getAuthToken, clearAuthToken } from "./auth";

// Use the default axios singleton so Orval-generated calls (which import 'axios') share interceptors.
axios.defaults.baseURL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:3000";

axios.interceptors.request.use((config) => {
  const token = getAuthToken();
  if (token) {
    config.headers = config.headers ?? {};
    (config.headers as Record<string, string>)["Authorization"] = `Bearer ${token}`;
  }
  return config;
});

axios.interceptors.response.use(
  (resp) => resp,
  (error) => {
    const status: number | undefined = error?.response?.status;
    if (status === 401 || status === 403) {
      clearAuthToken();
    }
    return Promise.reject(error);
  }
);

export default axios;



How It Works:

  1. Request Interceptor: axios.interceptors.request.use acts as a middleware for every outgoing request. Before any request is sent by our app, this function runs. It grabs the JWT using a helper function getAuthToken() (which you would implement to read from localStorage or cookies) and injects it into the request headers.
  2. Response Interceptor: axios.interceptors.response.use inspects every response coming back from the API. The second argument is an error handler. We check if the response status is 401 (Unauthorized) or 403 (Forbidden). If it is, we know our token is no longer valid. We then call clearAuthToken() to remove the stale token from storage.

You can inspect the real working code here.

Conclusion

By integrating Orval into our development workflow, we solve all the problems of the manual approach:

  • Single Source of Truth: The OpenAPI spec dictates the client code. Any API change is reflected in the client by simply re-running a command.
  • Zero Boilerplate: No more writing repetitive data models, fetch functions, or React Query hooks.
  • Full Type Safety: Eliminate an entire class of bugs caused by frontend/backend type mismatches.
  • Improved Developer Experience (DX): Spend less time on plumbing and more time building features. Autocomplete becomes your best friend.

Adopting a code-generation strategy for your API client is a game-changer for productivity and application stability. Give Orval a try in your next project—you'll never want to go back to writing an API layer by hand again.

Example Code

You can find a complete, working example of this setup in GitHub repository of Dojotek AI Chatbot, backend and frontend.

  1. package.json on Dojotek AI Chatbot frontend repo
  2. orval.config.js on Dojotek AI Chatbot frontend repo
  3. AdminChatbots.tsx on Dojotek AI Chatbot frontend repo for example of React Query hooks usage