Skip to content

SWAGGER / OPENAPI → TANSTACK QUERY

Your spec in. Typed hooks out.

Point it at a Swagger spec, run one command, get fully-typed TanStack Query code — one folder per controller, using your own axios instance.

npm i -D swagger-to-tanstack-query
  • Swagger 2.0
  • OpenAPI 3.x
  • TanStack Query v5
  • Bring your own axios
  • MIT

An OpenAPI getContact operation from swagger.json transforming into a typed getContact query function in contact/apis.ts.

Everything typed, nothing hand-written.

  • Controller-based output

    One folder per OpenAPI tag — each self-contained with its own types, apis, queries and mutations.

  • TanStack Query v5

    GET / HEAD become queryOptions; POST / PUT / PATCH / DELETE become useXxx mutation hooks.

  • Bring your own axios

    baseURL, auth and interceptors stay in your instance. Generated code just imports it.

  • Response envelope unwrapping

    Return the inner payload, not { data, message, … } — type-safe through a generic envelope.

  • Typed errors

    Every hook's error is typed as AxiosError<YourErrorType>, applied per hook.

  • Faithful types

    $ref, allOf / oneOf / anyOf, enums, nullable, arrays, maps and binary → Blob.

  • Header params & uploads

    in: header params via axios config; multipart/form-data assembled as FormData.

  • Docs preserved

    summary and @deprecated from the spec carry through as JSDoc on the generated code.

  • Swagger 2.0 & OpenAPI 3.x

    Both spec dialects are parsed and normalized to the same typed output.

  • Safe identifiers

    Reserved words and wire-name mismatches (page-size, delete) are handled correctly.

From spec to hooks in three steps.

  1. 01

    Add a config file

    Point swagger-to-tanstack-query.config.json at your spec and your axios instance.

    swagger-to-tanstack-query.config.json json
    {
      "url": "https://api.example.com/v3/api-docs",
      "output": "./src/api",
      "client": { "path": "@/lib/axios", "name": "axiosInstance" },
      "response": {
        "dataField": "data",
        "envelope": { "path": "@/lib/axios", "name": "CommonResponse" }
      },
      "error": { "path": "@/lib/axios", "name": "ApiError" }
    }
  2. 02

    Run one command

    Generate the full typed client — one folder per controller.

    terminal bash
    $ npm run codegen
    
    swagger-to-tanstack-query
      spec   : https://api.example.com/v3/api-docs
      output : ./src/api
      client : axiosInstance from "@/lib/axios"
      generating...
      done. 13 controllers, 65 files.
  3. 03

    Use the hooks

    Drop the generated queryOptions straight into useQuery.

    ContactName.tsx tsx
    import { useQuery } from "@tanstack/react-query";
    import { contactQueries } from "@/api/contact";
    
    const { data } = useQuery(
      contactQueries.getContact({ contactId: 1 }),
    );
    //      ^? Detail | undefined

One folder per controller.

The real generated output for a contact controller — types, api functions, queryOptions, mutation hooks and a barrel.

contact/types.ts ts
// contact/types.ts
/** Common API response envelope */
export interface Detail {
  id?: number;
  name: string;
  status?: "ACTIVE" | "ARCHIVED" | "DELETED";
  tags?: Array<Tag>;
}
contact/apis.ts ts
// contact/apis.ts
import { axiosInstance as client } from "@/lib/axios";
import type { CommonResponse } from "@/lib/axios";
import type { Detail, Create } from "./types";

/** Get contact details */
export const getContact = ({ contactId }: { contactId: number }) =>
  client.get<CommonResponse<Detail>>(`/api/v1/contacts/${contactId}`).then((res) => res.data.data);

/** Create a contact */
export const createContact = ({ body }: { body: Create }) =>
  client.post<CommonResponse<Create>>(`/api/v1/contacts`, body).then((res) => res.data.data);
contact/queries.ts ts
// contact/queries.ts
import { queryOptions } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import type { ApiError } from "@/lib/axios";
import * as apis from "./apis";

export const contactQueries = {
  getContact: (args: { contactId: number }) =>
    queryOptions<Awaited<ReturnType<typeof apis.getContact>>, AxiosError<ApiError>>({
      queryKey: ["contact", "getContact", args],
      queryFn: () => apis.getContact(args),
    }),
};
contact/mutations.ts ts
// contact/mutations.ts
import { useMutation } from "@tanstack/react-query";
import type { UseMutationOptions } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import type { ApiError } from "@/lib/axios";
import * as apis from "./apis";
import type { Create } from "./types";

/** Create a contact */
export const useCreateContact = (
  options?: Omit<
    UseMutationOptions<Awaited<ReturnType<typeof apis.createContact>>, AxiosError<ApiError>, { body: Create }>,
    "mutationFn"
  >,
) =>
  useMutation({
    mutationFn: (vars: { body: Create }) => apis.createContact(vars),
    ...options,
  });
contact/index.ts ts
// contact/index.ts
export * from "./types";
export * from "./apis";
export * from "./queries";
export * from "./mutations";

Stop writing fetch wrappers.