npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tanstack-api-generator

v2.0.0

Published

Automatic code generation library for TanStack Query v5+ that eliminates manual boilerplate

Readme

tanstack-api-generator

Automatic code generation library for TanStack Query v5+ that eliminates manual boilerplate. Generate fully-typed React Query hooks, query keys, fetchers, and invalidation helpers from a simple typed configuration object.

Features

  • 🎯 Zero Boilerplate - Define your API once, get fully-typed hooks automatically
  • 🔒 Type-Safe - Complete TypeScript inference from configuration to hooks
  • ⌨️ Full Autocompletion - Request bodies, query parameters, and responses all typed
  • 🔑 Smart Query Keys - Deterministic, hierarchical keys following TanStack Query v5 standards
  • ♻️ Auto Invalidation - Mutations automatically invalidate related queries
  • 🎣 Flexible Interceptors - Add authentication, logging, or custom headers easily
  • Request & Response Validation - Runtime type safety with Zod schemas
  • 📦 Tiny Bundle - Minimal runtime overhead beyond TanStack Query itself

Installation

npm install tanstack-api-generator @tanstack/react-query
# or
yarn add tanstack-api-generator @tanstack/react-query
# or
pnpm add tanstack-api-generator @tanstack/react-query

Optional: Install Zod for runtime validation

npm install zod

Quick Start

1. Define Your API Configuration

import { createQueryAPI } from "tanstack-api-generator";

// Minimal configuration - just define your endpoints
const api = createQueryAPI(
  {
    users: {
      list: { method: "GET", path: "/users" },
      get: { method: "GET", path: "/users/:id" },
      create: { method: "POST", path: "/users" },
      update: { method: "PUT", path: "/users/:id" },
      delete: { method: "DELETE", path: "/users/:id" },
    },
    posts: {
      list: { method: "GET", path: "/posts" },
      get: { method: "GET", path: "/posts/:id" },
    },
  } as const,
  {
    baseURL: "https://api.example.com",
  }
);

Note: The as const assertion is required for full type inference. The second parameter (fetch config) is optional - you can start with just the endpoint configuration. See the Zero-Config Philosophy section below for more details.

2. Use Generated Hooks in Your Components

import { api } from "./api";

function UserList() {
  // GET requests become useQuery hooks
  const { data: users, isLoading } = api.users.list.useQuery();

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

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

function UserDetail({ userId }: { userId: string }) {
  // Path parameters are automatically typed
  const { data: user } = api.users.get.useQuery({ id: userId });

  return <div>{user?.name}</div>;
}

function CreateUser() {
  // POST/PUT/PATCH/DELETE become useMutation hooks
  const createUser = api.users.create.useMutation();

  const handleSubmit = (name: string) => {
    createUser.mutate({ body: { name } });
  };

  return <button onClick={() => handleSubmit("John")}>Create User</button>;
}

That's it! No manual hook creation, no query key management, no invalidation logic needed.

💡 Pro Tip: Add bodySchema and querySchema to your endpoints for full TypeScript autocompletion on request bodies and query parameters. See Request and Response Typing for details.

Core Concepts

Zero-Config Philosophy

The library is designed to work with minimal configuration. You only need to define your endpoints - everything else has sensible defaults:

// Minimal - works out of the box
const api = createQueryAPI({
  users: {
    list: { method: "GET", path: "/users" },
  },
} as const);

// With optional baseURL
const api = createQueryAPI(
  {
    users: {
      list: { method: "GET", path: "/users" },
    },
  } as const,
  { baseURL: "https://api.example.com" }
);

API Configuration

Define your API endpoints using a simple configuration object:

const config = {
  [group]: {
    [endpoint]: {
      method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
      path: "/path/with/:params",
      schema: zodSchema, // Optional Zod validation
    },
  },
} as const;

Important: Use as const to enable full type inference.

Path Parameters

Path parameters are automatically extracted and typed:

const api = createQueryAPI({
  users: {
    get: { method: "GET", path: "/users/:id" },
    posts: { method: "GET", path: "/users/:userId/posts/:postId" },
  },
} as const);

// TypeScript enforces parameter types
api.users.get.useQuery({ id: "123" }); // ✅
api.users.get.useQuery({ id: 123 }); // ✅
api.users.get.useQuery(); // ❌ Error: missing required parameter

api.users.posts.useQuery({ userId: "1", postId: "2" }); // ✅

Query Keys

Query keys are automatically generated following TanStack Query v5 standards:

// Access query keys for manual cache operations
const listKey = api.users.list.key(); // ['users', 'list']
const getKey = api.users.get.key({ id: "123" }); // ['users', 'get', { id: '123' }]

// Use with queryClient
queryClient.invalidateQueries({ queryKey: listKey });

Automatic Invalidation

Mutations automatically invalidate related queries:

const createUser = api.users.create.useMutation();
const updateUser = api.users.update.useMutation();
const deleteUser = api.users.delete.useMutation();

// POST mutations invalidate list queries
createUser.mutate({ body: { name: "John" } });
// Automatically invalidates: api.users.list

// PUT/PATCH mutations invalidate list and specific item
updateUser.mutate({ params: { id: "123" }, body: { name: "Jane" } });
// Automatically invalidates: api.users.list and api.users.get({ id: '123' })

// DELETE mutations invalidate list and specific item
deleteUser.mutate({ params: { id: "123" } });
// Automatically invalidates: api.users.list and api.users.get({ id: '123' })

Manual Invalidation

For more control, use manual invalidation utilities:

// Invalidate specific endpoint
await api.users.invalidate.list();
await api.users.invalidate.get({ id: "123" });

// Invalidate entire group
await api.users.invalidate.all();

Common Use Cases

Fetching a List of Items

function UserList() {
  const { data, isLoading, error } = api.users.list.useQuery();

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

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

Fetching a Single Item with Parameters

function UserProfile({ userId }: { userId: string }) {
  // Path parameters are fully typed
  const { data: user } = api.users.get.useQuery({ id: userId });

  return <div>{user?.name}</div>;
}

Creating a New Item

function CreateUserForm() {
  const createUser = api.users.create.useMutation({
    onSuccess: (newUser) => {
      console.log("Created:", newUser);
      // List query is automatically invalidated!
    },
  });

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    createUser.mutate({
      body: {
        name: formData.get("name"),
        email: formData.get("email"),
      },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit" disabled={createUser.isPending}>
        {createUser.isPending ? "Creating..." : "Create User"}
      </button>
    </form>
  );
}

Updating an Existing Item

function EditUser({ userId }: { userId: string }) {
  const { data: user } = api.users.get.useQuery({ id: userId });
  const updateUser = api.users.update.useMutation();

  const handleUpdate = (name: string) => {
    updateUser.mutate({
      params: { id: userId },
      body: { name },
    });
    // Both list and detail queries are automatically invalidated!
  };

  return (
    <div>
      <input
        defaultValue={user?.name}
        onBlur={(e) => handleUpdate(e.target.value)}
      />
    </div>
  );
}

Deleting an Item

function DeleteUserButton({ userId }: { userId: string }) {
  const deleteUser = api.users.delete.useMutation({
    onSuccess: () => {
      console.log("User deleted");
      // Both list and detail queries are automatically invalidated!
    },
  });

  return (
    <button
      onClick={() => deleteUser.mutate({ params: { id: userId } })}
      disabled={deleteUser.isPending}
    >
      Delete
    </button>
  );
}

Conditional Queries

function UserProfile({ userId }: { userId: string | null }) {
  // Only fetch when userId is available
  const { data: user } = api.users.get.useQuery(
    { id: userId! },
    {
      enabled: !!userId,
    }
  );

  return user ? <div>{user.name}</div> : <div>No user selected</div>;
}

Polling and Refetching

function LiveUserList() {
  const { data: users } = api.users.list.useQuery(undefined, {
    refetchInterval: 5000, // Refetch every 5 seconds
    refetchOnWindowFocus: true,
  });

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

Advanced Usage

Authentication with Interceptors

The beforeRequest interceptor lets you modify requests before they're sent. This is perfect for adding authentication:

const api = createQueryAPI(config, {
  baseURL: "https://api.example.com",
  beforeRequest: async (config) => {
    // Get token from your auth system
    const token = await getAuthToken();

    return {
      ...config,
      headers: {
        ...config.headers,
        Authorization: `Bearer ${token}`,
      },
    };
  },
});

Real-world example with localStorage:

const api = createQueryAPI(config, {
  baseURL: "https://api.example.com",
  beforeRequest: async (config) => {
    const token = localStorage.getItem("authToken");

    if (token) {
      return {
        ...config,
        headers: {
          ...config.headers,
          Authorization: `Bearer ${token}`,
        },
      };
    }

    return config;
  },
});

Response Transformation and Logging

The afterResponse interceptor lets you transform or log responses:

const api = createQueryAPI(config, {
  baseURL: "https://api.example.com",
  afterResponse: async (response) => {
    // Log all API calls
    console.log(`[API] ${response.status} ${response.url}`);

    // Handle global error cases
    if (response.status === 401) {
      // Redirect to login
      window.location.href = "/login";
    }

    return response;
  },
});

Advanced error handling:

const api = createQueryAPI(config, {
  baseURL: "https://api.example.com",
  afterResponse: async (response) => {
    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      throw new Error(errorData.message || `HTTP ${response.status}`);
    }
    return response;
  },
});

Request and Response Typing

Get complete type safety for both request bodies and response data with automatic TypeScript inference.

Response Typing with schema

Add Zod schemas for runtime type safety and automatic validation of API responses:

import { z } from "zod";

// Define your schemas
const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
  createdAt: z.string().datetime(),
});

const api = createQueryAPI({
  users: {
    list: {
      method: "GET",
      path: "/users",
      schema: z.array(UserSchema), // Validates array of users
    },
    get: {
      method: "GET",
      path: "/users/:id",
      schema: UserSchema, // Validates single user
    },
    create: {
      method: "POST",
      path: "/users",
      schema: UserSchema, // Validates created user response
    },
  },
} as const);

// TypeScript knows the exact shape of data
const { data } = api.users.list.useQuery();
// data is typed as Array<{ id: string; name: string; email: string; createdAt: string }>

Benefits of response schemas:

  • Runtime type safety - catch API contract violations
  • Automatic type inference - no need to manually type responses
  • Data transformation - Zod can coerce and transform data
  • Validation errors - get detailed error messages when validation fails

Request Body Typing with bodySchema

Type your request bodies for POST, PUT, and PATCH requests with full autocompletion:

import { z } from "zod";

// Define request body schema
const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().min(0).optional(),
});

const UpdateUserSchema = z.object({
  name: z.string().min(1).optional(),
  email: z.string().email().optional(),
  age: z.number().min(0).optional(),
});

const api = createQueryAPI({
  users: {
    create: {
      method: "POST",
      path: "/users",
      bodySchema: CreateUserSchema, // Request body validation
      schema: UserSchema, // Response validation
    },
    update: {
      method: "PUT",
      path: "/users/:id",
      bodySchema: UpdateUserSchema, // Request body validation
      schema: UserSchema, // Response validation
    },
  },
} as const);

// Full TypeScript autocompletion for request body!
const createUser = api.users.create.useMutation();

createUser.mutate({
  body: {
    name: "John Doe", // ✅ Autocompleted
    email: "[email protected]", // ✅ Autocompleted
    age: 30, // ✅ Autocompleted and optional
    // invalidProp: true // ❌ TypeScript error: property doesn't exist
  },
});

// With path parameters and body
const updateUser = api.users.update.useMutation();

updateUser.mutate({
  params: { id: "123" }, // ✅ Typed from path
  body: {
    name: "Jane Doe", // ✅ Autocompleted
    email: "[email protected]", // ✅ Autocompleted
  },
});

Benefits of bodySchema:

  • Full TypeScript autocompletion in your IDE
  • Compile-time type checking - catch errors before runtime
  • Runtime validation - ensure data matches schema before sending
  • No manual type casting needed
  • Prevents sending invalid data to your API

Query Parameter Typing with querySchema

Type your query parameters for GET requests with full autocompletion:

import { z } from "zod";

// Define query parameter schema
const UserFilterSchema = z.object({
  name: z.string().optional(),
  email: z.string().optional(),
  page: z.number().optional(),
  limit: z.number().optional(),
  sortBy: z.enum(["name", "email", "createdAt"]).optional(),
});

const api = createQueryAPI({
  users: {
    list: {
      method: "GET",
      path: "/users",
      querySchema: UserFilterSchema, // Query params validation
      schema: z.array(UserSchema), // Response validation
    },
  },
} as const);

// Full TypeScript autocompletion for query parameters!
const { data: users } = api.users.list.useQuery({
  name: "John", // ✅ Autocompleted
  page: 1, // ✅ Autocompleted
  limit: 10, // ✅ Autocompleted
  sortBy: "name", // ✅ Autocompleted with enum values
  // invalid: true // ❌ TypeScript error: property doesn't exist
});

// Query params are included in the query key for proper caching
const key = api.users.list.key({ name: "John", page: 1 });
// ['users', 'list', { name: 'John', page: 1 }]

Benefits of querySchema:

  • Full TypeScript autocompletion for query parameters
  • Type-safe filtering and pagination
  • Automatic query key generation with params
  • Runtime validation of query parameters
  • Prevents invalid query parameters

Complete Example with All Typing Features

import { z } from "zod";

// Define all schemas
const UserFilterSchema = z.object({
  search: z.string().optional(),
  page: z.number().optional(),
  limit: z.number().optional(),
});

const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().min(0),
});

const UpdateUserSchema = z.object({
  name: z.string().min(1).optional(),
  email: z.string().email().optional(),
  age: z.number().min(0).optional(),
});

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
  age: z.number(),
  createdAt: z.string(),
});

// Create fully-typed API
const api = createQueryAPI({
  users: {
    list: {
      method: "GET",
      path: "/users",
      querySchema: UserFilterSchema, // Query params typed
      schema: z.array(UserSchema), // Response typed
    },
    get: {
      method: "GET",
      path: "/users/:id",
      schema: UserSchema, // Response typed
    },
    create: {
      method: "POST",
      path: "/users",
      bodySchema: CreateUserSchema, // Request body typed
      schema: UserSchema, // Response typed
    },
    update: {
      method: "PUT",
      path: "/users/:id",
      bodySchema: UpdateUserSchema, // Request body typed
      schema: UserSchema, // Response typed
    },
  },
} as const);

// Use with full type safety
function UserManagement() {
  // Typed query with query parameters
  const { data: users } = api.users.list.useQuery({
    search: "john",
    page: 1,
    limit: 10,
  });

  // Typed mutation with body
  const createUser = api.users.create.useMutation();

  // Typed mutation with params and body
  const updateUser = api.users.update.useMutation();

  const handleCreate = () => {
    createUser.mutate({
      body: {
        name: "John Doe",
        email: "[email protected]",
        age: 30,
      },
    });
  };

  const handleUpdate = (userId: string) => {
    updateUser.mutate({
      params: { id: userId },
      body: {
        name: "Jane Doe",
      },
    });
  };

  return (
    <div>
      {users?.map((user) => (
        <div key={user.id}>
          {user.name} - {user.email}
          <button onClick={() => handleUpdate(user.id)}>Update</button>
        </div>
      ))}
      <button onClick={handleCreate}>Create User</button>
    </div>
  );
}

Validation is Optional

You can use the library without schemas - data will be typed as unknown:

// Without schemas - data is typed as `unknown`
const api = createQueryAPI({
  users: {
    list: { method: "GET", path: "/users" },
    create: { method: "POST", path: "/users" },
  },
} as const);

// You can still use it, just without runtime validation
const { data } = api.users.list.useQuery();
// data is `unknown` - you'll need to type it yourself

const createUser = api.users.create.useMutation();
createUser.mutate({ body: userData }); // body is `unknown`

Combining Interceptors and Validation

Here's a complete real-world example combining authentication, logging, and validation:

import { z } from "zod";

// Define schemas
const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
});

// Create API with all features
const api = createQueryAPI(
  {
    users: {
      list: {
        method: "GET",
        path: "/users",
        schema: z.array(UserSchema),
      },
      get: {
        method: "GET",
        path: "/users/:id",
        schema: UserSchema,
      },
      create: {
        method: "POST",
        path: "/users",
        schema: UserSchema,
      },
    },
  } as const,
  {
    baseURL: "https://api.example.com",
    headers: {
      "Content-Type": "application/json",
    },
    beforeRequest: async (config) => {
      // Add authentication
      const token = localStorage.getItem("authToken");
      if (token) {
        config.headers = {
          ...config.headers,
          Authorization: `Bearer ${token}`,
        };
      }

      // Log outgoing requests
      console.log(`[API Request] ${config.method} ${config.url}`);

      return config;
    },
    afterResponse: async (response) => {
      // Log responses
      console.log(`[API Response] ${response.status} ${response.url}`);

      // Handle auth errors globally
      if (response.status === 401) {
        localStorage.removeItem("authToken");
        window.location.href = "/login";
      }

      return response;
    },
  }
);

// Now use it with full type safety and validation
function UserList() {
  const { data: users } = api.users.list.useQuery();
  // users is fully typed and validated!

  return (
    <ul>
      {users?.map((user) => (
        <li key={user.id}>{user.email}</li>
      ))}
    </ul>
  );
}

Custom Headers

Add default headers to all requests:

const api = createQueryAPI(config, {
  baseURL: "https://api.example.com",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": "your-api-key",
    "X-Client-Version": "1.0.0",
  },
});

TanStack Query Options

Pass any TanStack Query options to hooks:

// useQuery options
const { data } = api.users.list.useQuery(undefined, {
  staleTime: 5000,
  refetchOnWindowFocus: false,
  enabled: isAuthenticated,
});

// useMutation options
const createUser = api.users.create.useMutation({
  onSuccess: (data) => {
    console.log("User created:", data);
  },
  onError: (error) => {
    console.error("Failed to create user:", error);
  },
});

Custom QueryClient

Provide your own QueryClient instance:

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60000,
    },
  },
});

const api = createQueryAPI(config, fetchConfig, queryClient);

API Reference

createQueryAPI(config, fetchConfig?, queryClient?)

Creates a fully-typed API client with auto-generated hooks.

Parameters:

  • config - API configuration object defining all endpoints
  • fetchConfig (optional) - Fetch wrapper configuration
  • queryClient (optional) - Custom QueryClient instance

Returns: Generated API object with typed hooks, keys, and utilities

FetchConfig

Configuration for the fetch wrapper:

type FetchConfig = {
  baseURL?: string;
  headers?: Record<string, string>;
  beforeRequest?: (config: RequestInit) => RequestInit | Promise<RequestInit>;
  afterResponse?: (response: Response) => Response | Promise<Response>;
};

EndpointConfig

Configuration for a single endpoint:

type EndpointConfig = {
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
  path: string;
  schema?: z.ZodSchema; // Response validation and type inference
  bodySchema?: z.ZodSchema; // Request body validation (POST/PUT/PATCH only)
  querySchema?: z.ZodSchema; // Query parameter validation and type inference
};

Property Details:

  • method - HTTP method for the endpoint
  • path - URL path with optional parameters (e.g., /users/:id)
  • schema (optional) - Zod schema for response validation and TypeScript type inference
  • bodySchema (optional) - Zod schema for request body validation and type inference
    • Only valid for POST, PUT, and PATCH methods
    • Provides full TypeScript autocompletion for mutation bodies
    • Validates request data before sending
  • querySchema (optional) - Zod schema for query parameter validation and type inference
    • Typically used with GET requests
    • Provides full TypeScript autocompletion for query parameters
    • Automatically included in query keys for proper caching

Error Handling

Errors are automatically mapped to structured error objects:

const { error } = api.users.get.useQuery({ id: "123" });

if (error) {
  console.log(error.status); // HTTP status code
  console.log(error.statusText); // Status text
  console.log(error.message); // Error message
  console.log(error.data); // Response data if available
}

Troubleshooting Type Errors

Common TypeScript Errors and Solutions

Error: "Property does not exist on type"

Problem: Trying to access a property that doesn't exist in your schema.

const { data: user } = api.users.get.useQuery({ id: "123" });
console.log(user.invalidProperty); // ❌ Error: Property 'invalidProperty' does not exist

Solution: Check your schema definition and ensure the property exists:

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
  // Add the missing property
  invalidProperty: z.string().optional(),
});

Error: "Argument of type X is not assignable to parameter of type Y"

Problem: Passing incorrect type to mutation body or query parameters.

createUser.mutate({
  body: {
    name: "John",
    age: "30", // ❌ Error: Type 'string' is not assignable to type 'number'
  },
});

Solution: Match the type defined in your schema:

createUser.mutate({
  body: {
    name: "John",
    age: 30, // ✅ Correct type
  },
});

Error: "Property 'X' is missing in type"

Problem: Required property is missing from request body.

createUser.mutate({
  body: {
    name: "John",
    // ❌ Error: Property 'email' is missing
  },
});

Solution: Add the required property or make it optional in your schema:

// Option 1: Add the required property
createUser.mutate({
  body: {
    name: "John",
    email: "[email protected]", // ✅ Added required property
  },
});

// Option 2: Make it optional in schema
const CreateUserSchema = z.object({
  name: z.string(),
  email: z.string().optional(), // Now optional
});

Error: "bodySchema is not allowed for GET/DELETE methods"

Problem: Trying to use bodySchema with GET or DELETE requests.

const api = createQueryAPI({
  users: {
    list: {
      method: "GET",
      bodySchema: UserSchema, // ❌ Error: bodySchema not allowed for GET
    },
  },
} as const);

Solution: Use querySchema for GET requests instead:

const api = createQueryAPI({
  users: {
    list: {
      method: "GET",
      querySchema: UserFilterSchema, // ✅ Use querySchema for GET
      schema: z.array(UserSchema),
    },
  },
} as const);

Error: "Expected 1-2 arguments, but got 0"

Problem: Missing required parameters for query or mutation.

// With path parameters
const { data } = api.users.get.useQuery(); // ❌ Error: Expected 1 argument

Solution: Provide the required parameters:

// For path parameters
const { data } = api.users.get.useQuery({ id: "123" }); // ✅

// For query parameters
const { data } = api.users.list.useQuery({ page: 1 }); // ✅

// For no parameters (use undefined)
const { data } = api.users.list.useQuery(undefined, {
  staleTime: 5000,
}); // ✅

Error: "Type 'unknown' is not assignable to type X"

Problem: Response data is typed as unknown because no schema is provided.

const { data } = api.users.list.useQuery();
const name: string = data[0].name; // ❌ Error: data is unknown

Solution: Add a schema to your endpoint configuration:

const api = createQueryAPI({
  users: {
    list: {
      method: "GET",
      path: "/users",
      schema: z.array(UserSchema), // ✅ Add schema for type inference
    },
  },
} as const);

// Now data is properly typed
const { data } = api.users.list.useQuery();
const name: string = data?.[0].name; // ✅ Works!

Error: "Cannot find name 'z'"

Problem: Zod is not imported.

const UserSchema = z.object({
  // ❌ Error: Cannot find name 'z'
  id: z.string(),
});

Solution: Import Zod at the top of your file:

import { z } from "zod"; // ✅ Import Zod

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
});

Error: "Type instantiation is excessively deep and possibly infinite"

Problem: TypeScript is having trouble inferring types, usually due to missing as const.

const api = createQueryAPI({
  users: {
    list: { method: "GET", path: "/users" },
  },
}); // ❌ Missing 'as const'

Solution: Add as const to your configuration:

const api = createQueryAPI({
  users: {
    list: { method: "GET", path: "/users" },
  },
} as const); // ✅ Add 'as const'

Best Practices for Type Safety

  1. Always use as const - This enables full type inference:

    const api = createQueryAPI(config as const); // ✅
  2. Define schemas for all endpoints - Get full type safety:

    const api = createQueryAPI({
      users: {
        list: {
          method: "GET",
          path: "/users",
          querySchema: UserFilterSchema, // ✅ Query params typed
          schema: z.array(UserSchema), // ✅ Response typed
        },
        create: {
          method: "POST",
          path: "/users",
          bodySchema: CreateUserSchema, // ✅ Request body typed
          schema: UserSchema, // ✅ Response typed
        },
      },
    } as const);
  3. Use optional properties correctly - Make properties optional in schema if they're not always required:

    const UpdateUserSchema = z.object({
      name: z.string().optional(), // ✅ Optional for partial updates
      email: z.string().email().optional(),
    });
  4. Handle undefined data - Always check for undefined before accessing properties:

    const { data: user } = api.users.get.useQuery({ id: "123" });
    
    // ✅ Safe access
    if (user) {
      console.log(user.name);
    }
    
    // ✅ Optional chaining
    console.log(user?.name);
    
    // ❌ Unsafe - might be undefined
    console.log(user.name);
  5. Use TypeScript 5.0+ - For best type inference and error messages:

    {
      "compilerOptions": {
        "target": "ES2020",
        "lib": ["ES2020", "DOM"],
        "strict": true
      }
    }

Getting Help

If you encounter a type error that's not covered here:

  1. Check that you're using TypeScript 5.0 or higher
  2. Ensure as const is added to your configuration
  3. Verify all schemas are properly defined
  4. Check the examples directory for working code
  5. Open an issue with a minimal reproduction

TypeScript

This library is built with TypeScript and provides complete type inference:

  • Path parameters are extracted and typed automatically
  • Response types are inferred from Zod schemas
  • Request body types are enforced based on HTTP method
  • All hooks have full TypeScript autocomplete

Requirements

  • React 18+
  • TanStack Query v5+
  • TypeScript 5.0+ (for best type inference)
  • Zod 3+ (optional, for runtime validation)

Documentation

Examples

Check out the examples directory for complete working examples:

  • basic-usage - Comprehensive example demonstrating all features
    • CRUD operations
    • Authentication with interceptors
    • Zod validation
    • Manual invalidation
    • Query key generation
    • All useQuery/useMutation properties

Run the example:

cd examples/basic-usage
npm install
npm run dev

License

MIT © Dominique Houessou

Contributing

Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.

Changelog

See CHANGELOG.md for a list of changes in each version.

Support

Acknowledgments

Built on top of the excellent TanStack Query library by Tanner Linsley.