Skip to content

Conversation

@edulennert
Copy link
Member

@edulennert edulennert commented Dec 18, 2025

Orval Guide

What is Orval?

Orval is a code generator that automatically creates TypeScript types and API client code from OpenAPI/Swagger specifications. It eliminates the need to manually write API client code and ensures type safety between your backend API and frontend code.

What Orval Does

  1. Reads OpenAPI/Swagger specs - Parses your API documentation (JSON/YAML format)
  2. Generates TypeScript types - Creates type definitions matching your API schemas
  3. Generates API client code - Creates functions/hooks to call your API endpoints
  4. Ensures type safety - Your frontend code is fully typed based on your API contract

Where Does Orval Get Types From?

Orval reads types from OpenAPI/Swagger specifications. In our project:

Source Options:

  1. Local OpenAPI JSON file (current setup):

    // orval.config.ts
    target: "./shared/api/openapi-dao.json";
  2. Remote OpenAPI endpoint:

    // When indexer is running
    target: "http://localhost:42069/docs";
    // Or production
    target: "https://obol-api-dev.up.railway.app/docs";
  3. Environment variable:

    target: process.env.INDEXER_OPENAPI_URL || "./shared/api/openapi-dao.json";

How OpenAPI Specs Are Created

In our backend (apps/indexer):

  • Hono framework with @hono/zod-openapi automatically generates OpenAPI specs
  • Endpoints are defined with Zod schemas (e.g., DaoResponseSchema)
  • The /docs endpoint serves the OpenAPI JSON specification
  • Example: apps/indexer/src/api/controllers/dao/index.ts defines the /dao endpoint

How to Generate Types

Step 1: Run Codegen Command

# From the dashboard directory
pnpm codegen

# Or from root
pnpm dashboard codegen

Step 2: Generated Files Location

After running codegen, Orval generates:

apps/dashboard/shared/api/generated/
├── governance/
│   └── governance.ts          # React Query hooks (useDao, etc.)
├── schemas/
│   ├── dao200.ts             # TypeScript types (Dao200)
│   └── index.ts               # Schema exports

Step 3: When to Regenerate

Run pnpm codegen whenever:

  • ✅ Backend API changes (new endpoints, modified schemas)
  • ✅ OpenAPI spec is updated
  • ✅ You want to sync frontend types with backend

Note: Generated files are marked with // Generated by orval - do not edit manually.

What Exactly Does Orval Generate?

1. TypeScript Types

File: shared/api/generated/schemas/dao200.ts

export type Dao200 = {
  id: string;
  chainId: number;
  quorum: string;
  proposalThreshold: string;
  votingDelay: string;
  votingPeriod: string;
  timelockDelay: string;
};

2. API Client Functions

File: shared/api/generated/governance/governance.ts

Plain Function (for non-React usage):

export const dao = (options?, signal?) => {
  return customInstance<Dao200>(
    { url: `/dao`, method: "GET", signal },
    options,
  );
};

React Query Hook:

export function useDao(options?) {
  // Returns: { data, isLoading, error, refetch, ... }
  // Fully typed with Dao200
}

3. Query Keys and Options

export const getDaoQueryKey = () => ['/dao'] as const;
export const getDaoQueryOptions = (options?) => { ... };

Configuration

Current Setup (orval.config.ts)

{
  indexer: {
    input: {
      target: process.env.INDEXER_OPENAPI_URL || "./shared/api/openapi-dao.json",
    },
    output: {
      mode: "tags-split",           // Split by OpenAPI tags
      target: "./shared/api/generated/indexer.ts",
      schemas: "./shared/api/generated/schemas",
      client: "react-query",         // Generate React Query hooks
      override: {
        mutator: {
          path: "./shared/api/mutator.ts",
          name: "customInstance",   // Custom axios instance
        },
      },
    },
  },
}

Available Client Types

Orval can generate different client types:

  • "react-query" - React Query hooks (current)
  • "swr" - SWR hooks
  • "axios" - Axios instances
  • "fetch" - Fetch-based clients
  • "svelte-query" - Svelte Query hooks

Pros

✅ Type Safety

  • End-to-end type safety from API to frontend
  • TypeScript catches API contract mismatches at compile time
  • Autocomplete for all API responses

✅ Automatic Code Generation

  • No manual API client code to write
  • Stays in sync with backend changes
  • Reduces boilerplate significantly

✅ Multiple Client Support

  • Can generate React Query, SWR, Axios, or plain functions
  • Choose the best fit for your project

✅ OpenAPI Standard

  • Works with any OpenAPI-compliant API
  • Industry standard format
  • Works with many backend frameworks

✅ Customizable

  • Custom mutators for authentication, headers, etc.
  • Configurable code generation
  • Can filter by tags, operations, etc.

Cons

❌ Requires OpenAPI Spec

  • Backend must expose OpenAPI/Swagger documentation
  • If backend doesn't have it, you need to create/maintain it manually

❌ Generated Code

  • Generated files can be large and complex
  • Less readable than hand-written code
  • Must regenerate when API changes

❌ Build Step Required

  • Need to run pnpm codegen after API changes
  • Can be forgotten, leading to type mismatches
  • Adds a step to development workflow

❌ Learning Curve

  • Need to understand OpenAPI spec format
  • Configuration can be complex
  • Debugging generated code is harder

Limitations

🚫 GraphQL Support

Can Orval be used with GraphQL?

No, Orval does NOT support GraphQL directly.

  • Orval is designed for REST APIs with OpenAPI/Swagger specs
  • GraphQL uses a different schema format (GraphQL Schema Definition Language)
  • For GraphQL, use tools like:
    • GraphQL Code Generator (already used in this project for GraphQL)
    • Apollo Codegen
    • GraphQL Tools

Other Limitations

  1. OpenAPI Only: Only works with OpenAPI 3.x / Swagger 2.x specs
  2. No Runtime Validation: Types are compile-time only (use Zod for runtime)
  3. Single Spec Per Config: Each config block handles one OpenAPI spec
  4. No Custom Logic: Generated code is purely functional - no business logic

Usage Example

Before Orval (Manual):

// Manual typing, error-prone
const fetchDao = async (daoId: string) => {
  const response = await axios.get("/dao", {
    headers: { "anticapture-dao-id": daoId },
  });
  return response.data; // No type safety!
};

After Orval (Type-Safe):

// Fully typed, autocomplete, type-checked
const { data, isLoading, error } = useDao({
  request: { daoId: DaoIdEnum.OBOL },
  query: { enabled: !!daoId },
});

// TypeScript knows: data is Dao200 | undefined
// Autocomplete works: data?.quorum, data?.chainId, etc.

Project-Specific Notes

Current Setup

  • Backend: Hono with @hono/zod-openapi generates OpenAPI specs
  • Frontend: Orval generates React Query hooks
  • Custom Mutator: shared/api/mutator.ts handles:
    • Base URL configuration
    • DAO ID header injection (anticapture-dao-id)
    • Custom axios instance

File Structure

apps/dashboard/
├── orval.config.ts                    # Orval configuration
├── shared/api/
│   ├── mutator.ts                     # Custom axios instance
│   ├── openapi-dao.json               # Local OpenAPI spec (fallback)
│   └── generated/                     # Generated files (gitignored?)
│       ├── governance/
│       │   └── governance.ts          # useDao hook
│       └── schemas/
│           └── dao200.ts              # Dao200 type

Environment Variables

  • INDEXER_OPENAPI_URL - Override OpenAPI spec source URL
  • NEXT_PUBLIC_INDEXER_URL - Override API base URL in mutator

Advanced Topics

Pagination

Orval handles pagination based on how your OpenAPI spec defines it. There are several pagination patterns:

1. Offset-Based Pagination (skip/limit)

OpenAPI Spec Example:

{
  "parameters": [
    {
      "name": "skip",
      "in": "query",
      "schema": { "type": "integer", "default": 0 }
    },
    {
      "name": "limit",
      "in": "query",
      "schema": { "type": "integer", "default": 20 }
    }
  ],
  "responses": {
    "200": {
      "schema": {
        "type": "object",
        "properties": {
          "items": { "type": "array" },
          "totalCount": { "type": "integer" }
        }
      }
    }
  }
}

Generated Hook:

const { data } = useProposals({
  request: { daoId },
  query: {
    skip: 0,
    limit: 20,
  },
});

// data.items - array of items
// data.totalCount - total count

2. Cursor-Based Pagination (after/before)

OpenAPI Spec Example:

{
  "parameters": [
    {
      "name": "after",
      "in": "query",
      "schema": { "type": "string" }
    },
    {
      "name": "before",
      "in": "query",
      "schema": { "type": "string" }
    },
    {
      "name": "limit",
      "in": "query",
      "schema": { "type": "integer", "default": 365 }
    }
  ]
}

Generated Hook:

const { data } = useDelegationPercentage({
  request: { daoId },
  query: {
    after: "2024-01-01",
    limit: 100,
  },
});

3. Infinite Queries (React Query)

To enable infinite queries for cursor-based pagination, configure Orval:

// orval.config.ts
override: {
  query: {
    useInfinite: true,              // Enable infinite queries
    useInfiniteQueryParam: "limit", // Parameter for page size
  },
}

Generated Infinite Hook:

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
} = useInfiniteDelegationPercentage({
  request: { daoId },
  query: {
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  },
});

// Access all pages: data.pages
// Load more: fetchNextPage()

Note: Currently configured with useInfinite: false - enable if needed for infinite scroll patterns.

Type System

Orval generates comprehensive TypeScript types from your OpenAPI schemas:

1. Basic Types

OpenAPI Schema:

{
  "Dao200": {
    "type": "object",
    "properties": {
      "id": { "type": "string" },
      "chainId": { "type": "number" },
      "quorum": { "type": "string" }
    },
    "required": ["id", "chainId", "quorum"]
  }
}

Generated Type:

export type Dao200 = {
  id: string;
  chainId: number;
  quorum: string;
};

2. Nested Types

OpenAPI Schema:

{
  "ProposalResponse": {
    "type": "object",
    "properties": {
      "id": { "type": "string" },
      "voters": {
        "type": "array",
        "items": {
          "$ref": "#/components/schemas/Voter"
        }
      }
    }
  },
  "Voter": {
    "type": "object",
    "properties": {
      "address": { "type": "string" },
      "weight": { "type": "string" }
    }
  }
}

Generated Types:

export type Voter = {
  address: string;
  weight: string;
};

export type ProposalResponse = {
  id: string;
  voters: Voter[];
};

3. Union Types

OpenAPI Schema:

{
  "status": {
    "oneOf": [
      { "type": "string", "enum": ["active", "pending"] },
      { "type": "null" }
    ]
  }
}

Generated Type:

type Status = "active" | "pending" | null;

4. Request/Response Types

Orval generates separate types for:

  • Request parameters (query, path, body)
  • Response types (200, 400, 404, etc.)
  • Error types
// Request type
type UseDaoRequest = {
  daoId?: DaoIdEnum;
};

// Response type
type Dao200 = { ... };

// Error type
type DaoQueryError = unknown;

5. Type Inference

React Query hooks infer types automatically:

const { data } = useDao({ ... });
// TypeScript knows: data is Dao200 | undefined

// You can also explicitly type:
const { data } = useDao<Dao200, AxiosError>({ ... });

Multiple Queries

1. Multiple Endpoints

Orval generates separate hooks for each endpoint:

// Each endpoint gets its own hook
const daoData = useDao({ request: { daoId } });
const proposals = useProposals({ request: { daoId } });
const transactions = useTransactions({ request: { daoId } });

2. Parallel Queries

Use React Query's parallel query pattern:

// All queries run in parallel
const daoQuery = useDao({ request: { daoId } });
const proposalsQuery = useProposals({ request: { daoId } });
const transactionsQuery = useTransactions({ request: { daoId } });

// Check loading state
const isLoading = daoQuery.isLoading || proposalsQuery.isLoading || transactionsQuery.isLoading;

3. Dependent Queries

Chain queries that depend on each other:

const { data: daoData } = useDao({ request: { daoId } });
const { data: proposals } = useProposals({
  request: { daoId },
  query: {
    enabled: !!daoData, // Only fetch after daoData loads
  },
});

4. Query Batching

React Query automatically batches queries with the same key:

// Multiple components calling same hook
// React Query deduplicates and batches requests
<Component1 /> {/* calls useDao */}
<Component2 /> {/* calls useDao */}
// Only one network request is made

5. Multiple OpenAPI Specs

You can configure multiple specs in orval.config.ts:

export default defineConfig({
  indexer: {
    input: { target: "./openapi-indexer.json" },
    output: { ... },
  },
  petition: {
    input: { target: "./openapi-petition.json" },
    output: {
      target: "./shared/api/generated/petition.ts",
      // Different config per spec
    },
  },
});

6. Conditional Queries

Use React Query's enabled option:

const { data } = useDao({
  request: { daoId },
  query: {
    enabled: !!daoId && isReady, // Conditional fetching
  },
});

7. Query Invalidation

Invalidate and refetch queries:

import { useQueryClient } from "@tanstack/react-query";

const queryClient = useQueryClient();

// Invalidate specific query
queryClient.invalidateQueries({ queryKey: getDaoQueryKey() });

// Invalidate all queries with prefix
queryClient.invalidateQueries({ queryKey: ["/dao"] });

Best Practices

  1. Version Control: Consider committing generated files OR adding to .gitignore
  2. CI/CD: Run pnpm codegen in CI to catch type mismatches
  3. Documentation: Keep OpenAPI specs well-documented
  4. Incremental Adoption: Start with one endpoint, expand gradually
  5. Type Safety: Always use generated types, avoid any
  6. Pagination: Choose pagination pattern (offset vs cursor) based on your needs
  7. Query Optimization: Use enabled, staleTime, and gcTime to optimize fetching
  8. Error Handling: Always handle error states in your components

Related Tools

  • GraphQL Code Generator - Similar tool for GraphQL (already in use)
  • Zod - Runtime validation (used in backend for schemas)
  • OpenAPI Generator - Alternative to Orval (more languages, less TypeScript-focused)

Resources

@vercel
Copy link

vercel bot commented Dec 18, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
anticapture Ready Ready Preview, Comment Dec 19, 2025 6:50pm
anticapture-storybook Ready Ready Preview, Comment Dec 19, 2025 6:50pm

@edulennert edulennert self-assigned this Dec 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants