Skip to content

TypeGen - TanStack Query emitter

TypeTarget.TanStackQuery emits TypeScript client code for @tanstack/react-query v5 from the same endpoint model used by TypeGen’s OpenAPI paths. It scans:

  • hand-written Minimal APIs (MapGet, MapPost, MapGroup, .WithName(...), .WithTags(...))
  • hand-written [ApiController] actions
  • [CrudApi] synthesis from ZibStack.NET.Dto

Use it with TypeTarget.TypeScript so request and response model files are generated in the same build.

Terminal window
npm install @tanstack/react-query
using Microsoft.AspNetCore.Mvc;
using ZibStack.NET.Dto;
using ZibStack.NET.TypeGen;
var workflow = app.MapGroup("/api/workflow").WithTags("Workflow");
workflow.MapGet("/workspaces/{workspaceId:guid}/items",
(Guid workspaceId,
[FromQuery] string? search,
[FromQuery] WorkItemState? state,
[FromQuery] string[]? labels,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20) =>
PaginatedResponse<WorkItemSummary>.Create(items, items.Count, page, pageSize))
.WithName("searchWorkItems")
.WithTags("Workflow");
workflow.MapPost("/workspaces/{workspaceId:guid}/items",
(Guid workspaceId, [FromBody] CreateWorkItemCommand body) => CreateItem(body))
.WithName("createWorkItem")
.WithTags("Workflow");
[GenerateTypes(Targets = TypeTarget.TypeScript
| TypeTarget.OpenApi
| TypeTarget.TanStackQuery,
OutputDir = "../client/src/api")]
public class WorkItemSummary
{
public Guid Id { get; set; }
public required string Title { get; set; } = "";
public WorkItemState State { get; set; }
public List<string> Labels { get; set; } = new();
}
[GenerateTypes(Targets = TypeTarget.TypeScript
| TypeTarget.OpenApi
| TypeTarget.TanStackQuery,
OutputDir = "../client/src/api")]
public class CreateWorkItemCommand
{
public required string Title { get; set; } = "";
public WorkItemState InitialState { get; set; }
}
public sealed class TypeGenConfig : ITypeGenConfigurator
{
public void Configure(ITypeGenBuilder b)
{
b.TypeScript(ts =>
{
ts.OutputDir = "../client/src/api";
ts.PropertyNameStyle = NameStyle.CamelCase;
});
b.TanStackQuery(q =>
{
q.OutputDir = "../client/src/api";
q.SingleFileName = "api.gen.ts";
q.BaseUrlExpression = "import.meta.env.VITE_API_URL";
// q.FileLayout = QueryFileLayout.SplitByTag;
// q.ApiClientImportPath = "./http-client";
// q.ApiClientName = "request";
});
}
}

The default fetch client evaluates BaseUrlExpression defensively. If the configured expression is missing, empty, or throws, it falls back to window.location.origin in browser environments, then to http://localhost for non-browser execution. You can still set BaseUrlExpression to a literal origin or import a custom client when you want stricter environment handling.

import { mutationOptions, queryOptions, useMutation, useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query';
import type { CreateWorkItemCommand } from './CreateWorkItemCommand';
import type { WorkItemState } from './WorkItemState';
import type { WorkItemSummary } from './WorkItemSummary';
export type PaginatedResponseOfWorkItemSummary = {
items: WorkItemSummary[];
totalCount: number;
page: number;
pageSize: number;
totalPages: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
};
export const workflowKeys = {
all: ['workflow'] as const,
searchWorkItems: (input: SearchWorkItemsInput) => [...workflowKeys.all, 'searchWorkItems', input] as const,
};
export type SearchWorkItemsInput = {
workspaceId: string;
search?: string;
state?: WorkItemState;
labels?: string[];
page?: number;
pageSize?: number;
};
export function searchWorkItems(input: SearchWorkItemsInput, signal?: AbortSignal): Promise<PaginatedResponseOfWorkItemSummary> {
return apiFetch<PaginatedResponseOfWorkItemSummary>(`/api/workflow/workspaces/${encodeURIComponent(String(input.workspaceId))}/items`, {
method: 'GET',
query: {
search: input.search,
state: input.state,
labels: input.labels,
page: input.page,
pageSize: input.pageSize,
},
signal,
});
}
export function searchWorkItemsOptions(input: SearchWorkItemsInput) {
return queryOptions({
queryKey: workflowKeys.searchWorkItems(input),
queryFn: ({ signal }) => searchWorkItems(input, signal),
});
}
export function useSearchWorkItems(input: SearchWorkItemsInput) {
return useQuery(searchWorkItemsOptions(input));
}

Mutations get a fetch function, mutationOptions, a React hook, and tag-wide invalidation:

export type CreateWorkItemInput = {
workspaceId: string;
body: CreateWorkItemCommand;
};
export function createWorkItem(input: CreateWorkItemInput, signal?: AbortSignal): Promise<WorkItemSummary> {
return apiFetch<WorkItemSummary>(`/api/workflow/workspaces/${encodeURIComponent(String(input.workspaceId))}/items`, {
method: 'POST',
body: input.body,
signal,
});
}
export function useCreateWorkItem() {
const queryClient = useQueryClient();
return useMutation({
...createWorkItemMutationOptions(),
onSuccess: async () => {
await invalidateWorkflowQueries(queryClient);
},
});
}
export function invalidateWorkflowQueries(queryClient: QueryClient) {
return queryClient.invalidateQueries({ queryKey: workflowKeys.all });
}
SettingDefaultPurpose
OutputDirTypeScript output dir, then first model output dirWhere query files are written
FileLayoutQueryFileLayout.SingleFileSingleFile or SplitByTag
SingleFileNameapi.gen.tsFile name for single-file mode
BaseUrlExpressionimport.meta.env.VITE_API_URLBase URL expression used by the default fetch client; falls back to window.location.origin when unset
ApiClientImportPathnullImport a custom client instead of emitting apiFetch
ApiClientNameapiFetchDefault or imported client function name
ModelsImportPathcomputedForce model type imports from one module
SchemasImportPathcomputedForce generated Zod schema imports from one module
PayloadValidationNoneNone, Responses, or RequestsAndResponses runtime parsing
EmitQueryOptionstrueEmit queryOptions(...) helpers
EmitMutationOptionstrueEmit mutationOptions(...) helpers
EmitHookstrueEmit useQuery / useMutation wrappers
EmitCacheHelperstrueEmit invalidation and prefetch helpers

Set ApiClientImportPath when your app already has auth, retry, tenant, or observability behavior in one HTTP client:

b.TanStackQuery(q =>
{
q.ApiClientImportPath = "@/lib/api-client";
q.ApiClientName = "request";
});

The imported function is called like this:

request<T>(path, {
method,
query,
headers,
body,
signal,
});

query values can be scalar or arrays. The generated default client appends arrays as repeated query-string keys and JSON-serializes request bodies. Route and query parameter types use the same primitive mapping as generated models; notably decimal maps to string to preserve precision.

Add TypeTarget.Zod to the request/response DTOs, install zod@^4.6.1, and opt in:

b.TanStackQuery(q =>
{
q.PayloadValidation = QueryPayloadValidation.RequestsAndResponses;
// Optional when TypeGen can compute the relative schema paths:
q.SchemasImportPath = "../validation/schemas";
});

Responses parses successful API payloads before returning them to TanStack Query. RequestsAndResponses additionally parses JSON request bodies before they are sent. None preserves the existing zero-Zod-dependency client.

For Minimal APIs, prefer .WithName("searchWorkItems") and .WithTags("Workflow"). The operation name becomes the function/options/hook base name, and the tag becomes the query-key group. Without .WithName(...), TypeGen derives a stable name from verb plus route segments.