Skip to content

Conversation

@Pewww
Copy link

@Pewww Pewww commented Nov 10, 2025

SWR v2 Plugin PR Summary

Overview

New SWR plugin for @hey-api/openapi-ts that generates type-safe SWR hooks and utilities from OpenAPI specifications. This plugin follows SWR's official best practices and recommended patterns.

Key Features

1. Core Functionality

  • SWR Keys: Generates key functions for cache management following SWR's array key pattern
  • useSWR Options: Creates options helpers for standard queries with automatic key and fetcher generation
  • useSWRMutation Options: Generates mutation options for POST/PUT/DELETE operations
  • useSWRInfinite Options: Provides pagination support with automatic page key generation

2. Architecture

Plugin Structure

packages/openapi-ts/src/plugins/swr/
├── config.ts           # Plugin configuration and defaults
├── index.ts            # Main exports
├── plugin.ts           # Plugin handler entry point
├── types.d.ts          # TypeScript type definitions
├── useType.ts          # Type utilities for operation data/response
└── v2/
    ├── plugin.ts              # v2 handler implementation
    ├── swrKey.ts              # Key generation logic
    ├── swrOptions.ts          # useSWR options generation
    ├── swrMutationOptions.ts  # useSWRMutation options generation
    └── swrInfiniteOptions.ts  # useSWRInfinite options generation

Key Design Decisions

SWR Key Pattern (v2)

  • Uses array keys with object serialization (SWR 1.1.0+)
  • No params: ['/api/users']
  • With params: ['/api/users/{id}', options]
  • Keys always return valid arrays (never null)
  • Conditional fetching controlled by consumers

Query Options (useSWR)

  • Generates key property using registered SWR key function
  • Generates fetcher property with async SDK function call
  • Automatically adds throwOnError: true to SDK calls
  • Handles both required and optional parameters correctly
  • Respects SDK plugin's responseStyle config (data vs object)

Mutation Options (useSWRMutation)

  • Follows SWR mutation patterns with key + arg-based fetcher
  • Keys: ['/api/users'] or ['/api/users/{id}', options]
  • Fetcher signature: async (_key, { arg }) => ...
  • Spreads both options and arg into SDK function call

Infinite Options (useSWRInfinite)

  • Only generated for operations with pagination support
  • getKey function signature matches SWR requirements
  • Merges pagination parameter into query params
  • Key structure: [path, { ...options, query: { ...query, page: pageIndex } }]
  • Fetcher extracts options from key array

3. Configuration Options

{
  name: 'swr',
  case: 'camelCase',
  comments: true,
  exportFromIndex: false,

  swrKeys: {
    enabled: true,
    exported: true,
    name: '{{name}}Key',
    case: 'camelCase'
  },

  swrOptions: {
    enabled: true,
    exported: true,
    name: '{{name}}Options',
    case: 'camelCase'
  },

  swrMutationOptions: {
    enabled: true,
    exported: true,
    name: '{{name}}Mutation',
    case: 'camelCase'
  },

  swrInfiniteOptions: {
    enabled: true,
    exported: true,
    name: '{{name}}Infinite',
    case: 'camelCase'
  }
}

4. Generated Code Examples

Simple Query (No Params)

export const getUsersKey = () => ["/users"];

export const getUsersOptions = () => ({
  key: getUsersKey(),
  fetcher: async () => {
    const { data } = await getUsers({ throwOnError: true });
    return data;
  }
});

Query with Required Params

export const getUserByIdKey = (options: GetUserByIdData) =>
  ["/users/{id}", options];

export const getUserByIdOptions = (options: GetUserByIdData) => ({
  key: getUserByIdKey(options),
  fetcher: async () => {
    const { data } = await getUserById({ ...options, throwOnError: true });
    return data;
  }
});

Mutation

export const createUserMutation = (options?: CreateUserData) => ({
  key: ['/users'],
  fetcher: async (_key, { arg }) => {
    const { data } = await createUser({ ...options, ...arg, throwOnError: true });
    return data;
  }
});

Infinite Query with Pagination

export const getUsersInfinite = (options?: GetUsersData) => ({
  getKey: (pageIndex: number, previousPageData: GetUsersResponse | null) =>
    ['/users', { ...options, query: { ...options?.query, page: pageIndex } }],
  fetcher: async (key: readonly [string, GetUsersData]) => {
    const { data } = await getUsers({ ...key[1], throwOnError: true });
    return data;
  }
});

Implementation Details

Dependencies

  • @hey-api/sdk - Required for SDK function generation
  • @hey-api/typescript - Required for TypeScript type generation
  • axios (external) - For AxiosError type import

Special Handling

  • SSE Operations: Skipped (not supported by SWR)
  • asClass Mode: Properly handles class-based SDK clients
  • Optional Parameters: Uses TypeScript optional chaining for safety
  • ESLint Comments: Adds file-level eslint-disable for unused vars to avoid formatter issues

Testing

Test Coverage

  • Test files located in: packages/openapi-ts-tests/main/test/plugins/swr/
  • Covers OpenAPI versions: 2.0.x, 3.0.x, 3.1.x
  • Tests different client modes: fetch, axios, asClass
  • Edge cases tested: no-params, body-path, nullable, path-only

Snapshot Tests

  • Generated code snapshots maintained in test directories
  • Recently synced and updated to match latest implementation

Usage Example

// openapi-ts.config.ts
export default {
  client: '@hey-api/client-fetch',
  input: './openapi.json',
  output: './src/client',
  plugins: [
    '@hey-api/sdk',
    '@hey-api/typescript',
    {
      name: 'swr',
      swrOptions: true,
      swrMutationOptions: true,
      swrInfiniteOptions: true,
    }
  ]
};

// In your React component
import useSWR from 'swr';
import useSWRMutation from 'swr/mutation';
import useSWRInfinite from 'swr/infinite';
import { getUsersOptions, createUserMutation, getUsersInfinite } from './client/swr.gen';

// Query
const { data, error } = useSWR(...Object.values(getUsersOptions()));

// Mutation
const { trigger } = useSWRMutation(...Object.values(createUserMutation()));

// Infinite
const { getKey, fetcher } = getUsersInfinite({ query: { status: 'active' } });
const { data, size, setSize } = useSWRInfinite(getKey, fetcher);

Documentation Status

  • Documentation page exists at: docs/openapi-ts/plugins/swr.md
  • Currently marked as "soon" with feature status placeholder
  • Needs to be updated with comprehensive usage guide

PR Checklist

Code Changes

  • Plugin implementation complete
  • Type definitions added
  • Configuration system implemented
  • Test coverage added
  • Snapshots updated and synced
  • Lint issues resolved

Testing

  • Unit tests for key generation
  • Snapshot tests for all OpenAPI versions
  • Edge case coverage

Breaking Changes

None - This is a new plugin, no breaking changes expected.

Migration Notes

If users were using an unofficial or custom SWR integration, they should:

  1. Remove custom SWR helper code
  2. Add the official swr plugin to their config
  3. Update imports to use generated code from swr.gen.ts
  4. Follow new key patterns (array keys with object serialization)

Related Issues

  • Original feature request: Issue SWR plugin #1479 (referenced in docs)
  • Any other related issues should be linked in the PR description

Please review the PR and let me know if anything needs to be revised.
I’ve confirmed that the basic queries and mutations work well, but I don’t think the current spec is perfect yet.
Since there are naturally areas to improve, if this PR is merged, I’d like to cautiously suggest releasing it without adding it to the docs for now.

Thank you for the great library!

@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@changeset-bot
Copy link

changeset-bot bot commented Nov 10, 2025

⚠️ No Changeset found

Latest commit: 5c53c2c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@dosubot dosubot bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Nov 10, 2025
@vercel
Copy link

vercel bot commented Nov 10, 2025

@Pewww is attempting to deploy a commit to the Hey API Team on Vercel.

A member of the Team first needs to authorize it.

@dosubot dosubot bot added the feature 🚀 New feature or request label Nov 10, 2025
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Pewww don't delete this file please, we want to separate by major version

@mrlubos
Copy link
Member

mrlubos commented Nov 10, 2025

@Pewww pnpm test:update

Comment on lines 51 to 68
const statement = $.const(symbolUseQueryFn.placeholder)
.export(symbolUseQueryFn.exported)
.$if(
plugin.config.comments && createOperationComment({ operation }),
(c, v) => c.describe(v as Array<string>),
)
.assign(
$.func().do(
$(symbolUseSwr.placeholder)
.call(
$.literal(operation.path),
$.func()
.async()
.do(...statements),
)
.return(),
),
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Pewww did you see this? it's much cleaner than the tsc module. Try to use it where possible please and let me know which things can't be ported yet. I'm yet to improve some of the type interfaces for example, but you should be already able to cut 10-30% of lines per file

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll check it and make the necessary revisions.
I’ll also go through the rest of the comments!

@mrlubos
Copy link
Member

mrlubos commented Nov 10, 2025

@Pewww the only other question I have is why and how you decided to use that query key design!

@Pewww
Copy link
Author

Pewww commented Nov 13, 2025

@mrlubos Sorry it took a bit of time to finish the revisions.
I’ve applied the changes you mentioned (moving the plugin back into the v2 folder and switching from tsc to the ts-dsl syntax).
Could you review current version of code?

Regarding the current query key design: as far as I understand, SWR allows any serializable value as a key.
Internally, it uses logic similar to unstable_serialize to stringify arrays and objects into a JSON-based cache key, so the valid range is fairly broad.
We just need to avoid non-pure values like Date, Map, etc.

Also, when injecting values from options, if a key contains dashes, you would need bracket notation to access it.
Rather than handling that inside the plugin, I thought it would be more flexible to just pass the options object through as-is.

Separately, it looks like some tests are failing, but they don’t seem related to my changes.
Could we take a look together?
I’m not sure if I need to run an update command on my side.
On my branch, running test:update, examples:generate, format, and lint:fix all completed without issues.


@mrlubos
Copy link
Member

mrlubos commented Nov 13, 2025

@Pewww Did you rebuild the package before running those commands?

@codecov
Copy link

codecov bot commented Nov 13, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 29.78%. Comparing base (06ed518) to head (5c53c2c).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2956      +/-   ##
==========================================
- Coverage   29.98%   29.78%   -0.21%     
==========================================
  Files         409      413       +4     
  Lines       35547    35894     +347     
  Branches     2075     2075              
==========================================
+ Hits        10658    10690      +32     
- Misses      24862    25177     +315     
  Partials       27       27              
Flag Coverage Δ
unittests 29.78% <ø> (-0.21%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Pewww
Copy link
Author

Pewww commented Nov 13, 2025

@Pewww Did you rebuild the package before running those commands?

@mrlubos Oh, I missed it. Thanks.

I’ve been thinking about it, handling previousPageData inside the getKey function in the useSWRInfinite pattern feels tricky.
Depending on what data previousPageData holds, we need to decide whether to return null, but doing that check directly there seems difficult — both type-wise and functionally.

In contrast, libraries like React Query seems to have a structure that makes it much easier for the plugin to handle such cases.
Do you have any good ideas?
Otherwise, I’m considering simply excluding infinite support for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 🚀 New feature or request size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants