# createEdenTanStackQuery (/docs/api-reference/create-eden-tanstack-query) # createEdenTanStackQuery [#createedentanstackquery] The main factory function that creates typed providers and hooks for using Eden with TanStack Query. ## Signature [#signature] ```ts function createEdenTanStackQuery(): CreateEdenTanStackQueryResult ``` ### Type Parameters [#type-parameters] | Parameter | Description | |-----------|-------------| | `TApp` | Your Elysia application type, extends `AnyElysia` | ### Returns [#returns] Returns an object containing: | Property | Type | Description | |----------|------|-------------| | `EdenProvider` | `React.FC>` | Provider component for wrapping your app | | `useEden` | `() => EdenOptionsProxy` | Hook to get the typed options proxy | | `useEdenClient` | `() => Treaty.Create` | Hook to get the raw Eden client | ## Usage [#usage] ### Basic Setup [#basic-setup] ```ts title="lib/eden.ts" import { createEdenTanStackQuery } from 'eden-tanstack-react-query' import { treaty } from '@elysiajs/eden' import type { App } from './server' // Create typed hooks and provider export const { EdenProvider, useEden, useEdenClient } = createEdenTanStackQuery() // Create the Eden client export const edenClient = treaty('http://localhost:3000') ``` ### Provider Setup [#provider-setup] ```tsx title="App.tsx" import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { EdenProvider, edenClient } from './lib/eden' const queryClient = new QueryClient() function App() { return ( ) } ``` ### Using in Components [#using-in-components] ```tsx title="UserList.tsx" import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useEden } from './lib/eden' function UserList() { const eden = useEden() const queryClient = useQueryClient() // Fully typed query const { data: users } = useQuery(eden.users.get.queryOptions()) // Fully typed mutation const createUser = useMutation({ ...eden.users.post.mutationOptions(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: eden.users.get.queryKey() }) } }) return (
    {users?.map(user =>
  • {user.name}
  • )}
) } ``` ## Types [#types] ### CreateEdenTanStackQueryResult [#createedentanstackqueryresult] | Property | Type | Description | |----------|------|-------------| | `EdenProvider` | `React.FC` | Provider component that supplies Eden client to children | | `useEden` | `() => EdenOptionsProxy` | Hook returning the options proxy with decorated routes | | `useEdenClient` | `() => Treaty.Create` | Hook returning the raw Eden Treaty client | ### EdenProviderProps [#edenproviderprops] | Property | Type | Description | |----------|------|-------------| | `client` | `Treaty.Create` | Eden Treaty client instance | | `queryClient` | `QueryClient` | Optional, deprecated, and unused. Pass it to `QueryClientProvider` instead. | | `children` | `React.ReactNode` | React children | ## How It Works [#how-it-works] 1. **Type Extraction**: The factory uses TypeScript's type inference to extract route definitions from your Elysia app type. 2. **Provider Creation**: Creates a React context provider that holds the Eden client and its options proxy. 3. **Proxy Generation**: When `useEden()` is called, it creates a proxy object that mirrors your API structure but returns TanStack Query options instead of making API calls directly. 4. **Method Decoration**: Each route is decorated with appropriate methods: - GET, HEAD, OPTIONS routes get `queryOptions`, `queryKey`, `queryFilter`, and infinite query variants - POST, PUT, PATCH, DELETE routes get `mutationOptions` and `mutationKey` ## Multiple API Instances [#multiple-api-instances] Each factory call creates separate Eden contexts, but all instances use the same query key format. Keys do not include the backend URL or Eden context. If two backends expose the same route with the same input, their queries collide in a shared `QueryClient`. Use a separate `QueryClient` for each backend. This example puts each API in a sibling provider scope. Both backends expose `GET /users` returning an array of users: ```tsx import { treaty } from '@elysiajs/eden' import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query' import { createEdenTanStackQuery } from 'eden-tanstack-react-query' import { useState } from 'react' import type { InternalApp } from './internal-server' import type { ExternalApp } from './external-server' const internalApi = createEdenTanStackQuery() const externalApi = createEdenTanStackQuery() const internalClient = treaty('http://localhost:3000') const externalClient = treaty('http://localhost:4000') function App() { const [internalQueryClient] = useState(() => new QueryClient()) const [externalQueryClient] = useState(() => new QueryClient()) return ( <> ) } function InternalUsers() { const eden = internalApi.useEden() const { data } = useQuery(eden.users.get.queryOptions()) return

Internal users: {data?.length}

} function ExternalUsers() { const eden = externalApi.useEden() const { data } = useQuery(eden.users.get.queryOptions()) return

External users: {data?.length}

} ``` `useQuery` and `useQueryClient` use the nearest `QueryClientProvider`. Nesting Eden providers does not select a cache. If one component queries both APIs, pass the corresponding `QueryClient` explicitly as the second argument to each `useQuery(options, queryClient)` call, and use that same client for invalidation or prefetching. ## See Also [#see-also] - [useEden](/docs/api-reference/use-eden) - The main hook for accessing query options - [EdenProvider](#edenproviderprops) - Provider component details # Overview (/docs/api-reference) # API Reference [#api-reference] This section provides comprehensive documentation for all exports from `eden-tanstack-react-query`. ## Core API [#core-api] ### Factory Function [#factory-function] | Export | Description | |--------|-------------| | [`createEdenTanStackQuery`](/docs/api-reference/create-eden-tanstack-query) | Main factory that creates typed providers and hooks | ### Hooks [#hooks] | Export | Description | |--------|-------------| | [`useEden`](/docs/api-reference/use-eden) | Hook returning the options proxy with decorated routes | | `useEdenClient` | Hook returning the raw Eden Treaty client | ### Options Factories [#options-factories] | Export | Description | |--------|-------------| | [`queryOptions`](/docs/api-reference/use-eden#queryoptions) | Method on query routes for creating query options | | [`mutationOptions`](/docs/api-reference/use-eden#mutationoptions) | Method on mutation routes for creating mutation options | | [`infiniteQueryOptions`](/docs/api-reference/use-eden#infinitequeryoptions) | Method on supported non-union cursor routes for creating infinite query options | ### Query Keys [#query-keys] | Export | Description | |--------|-------------| | [`getQueryKey`](/docs/api-reference/use-eden#querykey) | Generate query keys from path and input | | [`getMutationKey`](/docs/api-reference/use-eden#mutationkey) | Generate mutation keys from path | ### Types [#types] | Export | Description | |--------|-------------| | [`EdenQueryKey`](/docs/api-reference/types) | Query key structure type | | [`EdenMutationKey`](/docs/api-reference/types) | Mutation key structure type | | [`EdenFetchError`](/docs/api-reference/types) | Error type for Eden requests | | [`RouteDefinition`](/docs/api-reference/types) | Base route type structure | ## Quick Reference [#quick-reference] ### Query Routes (GET, HEAD, OPTIONS) [#query-routes-get-head-options] ```ts const eden = useEden() // Available methods on query routes eden.users.get.queryOptions(input, opts?) // Input is conditional on the route query eden.users.get.queryKey(input?) // For cache operations eden.users.get.queryFilter(input?, filters?) // For invalidation // Available for a non-union query schema with a usable top-level cursor field eden.posts.get.infiniteQueryOptions(input, opts) // For useInfiniteQuery eden.posts.get.infiniteQueryKey(input?, keyOpts?) // For cache operations eden.posts.get.infiniteQueryFilter(input?, filters?) ``` ### Mutation Routes (POST, PUT, PATCH, DELETE) [#mutation-routes-post-put-patch-delete] ```ts // Available methods on mutation routes eden.users.post.mutationOptions(opts?) // For useMutation eden.users.post.mutationKey() // For cache operations ``` ### Path Parameters [#path-parameters] ```ts // For routes with path params like /users/:id eden.users({ id: '123' }).get.queryOptions() // Nested params: /posts/:postId/comments/:commentId eden.posts({ postId: '1' }).comments({ commentId: '2' }).get.queryOptions() ``` ## Package Exports [#package-exports] All public APIs are exported from the main package entry point: ```ts import { // Main factory createEdenTanStackQuery, // Low-level factories (for advanced use) edenQueryOptions, edenMutationOptions, edenInfiniteQueryOptions, // Query key utilities getQueryKey, getMutationKey, // Types type EdenQueryKey, type EdenMutationKey, type EdenFetchError, type RouteDefinition, type EdenOptionsProxy, // ... and more } from 'eden-tanstack-react-query' ``` # Types (/docs/api-reference/types) # Types [#types] This page documents all exported types from `eden-tanstack-react-query`. ## Route Types [#route-types] ### RouteDefinition [#routedefinition] Base structure representing extracted type information from an Elysia route. ```ts interface RouteDefinition { input: unknown // Combined input type output: unknown // Response type error: unknown // Error type } ``` ### HttpQueryMethod [#httpquerymethod] HTTP methods used for query operations (read). ```ts type HttpQueryMethod = 'get' | 'options' | 'head' ``` ### HttpMutationMethod [#httpmutationmethod] HTTP methods used for mutation operations (write). ```ts type HttpMutationMethod = 'post' | 'put' | 'patch' | 'delete' ``` ### HttpMethod [#httpmethod] All HTTP methods. ```ts type HttpMethod = HttpQueryMethod | HttpMutationMethod ``` ## Error Types [#error-types] ### EdenFetchError [#edenfetcherror] Error type for Eden requests. It includes standard `Error` fields alongside the typed status code and response value. Eden derives `message` with `String(value)`, so object response bodies may produce `[object Object]`. ```ts interface EdenFetchError extends Error { status: TStatus value: TValue } ``` #### Usage [#usage] ```tsx import { useMutation } from '@tanstack/react-query' import type { EdenFetchError } from 'eden-tanstack-react-query' const { mutate, error } = useMutation(eden.users.post.mutationOptions()) // error is typed as EdenFetchError if (error) { if (error.status === 400) { // Validation error console.log('Validation:', error.value) } else if (error.status === 500) { // Server error console.log('Server error:', error.value) } } ``` ## Query Key Types [#query-key-types] ### EdenQueryKey [#edenquerykey] Query key structure for TanStack Query cache. ```ts type EdenQueryKey = | [path: string[]] | [path: string[], meta: EdenQueryKeyMeta] // Examples: [['users', 'get']] [['users', 'get'], { input: { role: 'admin' } }] [['posts', 'get'], { input: { limit: 10 }, type: 'infinite', infinite: { initialPageParam: null } }] ``` ### EdenMutationKey [#edenmutationkey] Mutation key structure. ```ts type EdenMutationKey = [path: string[]] // Example: [['users', 'post']] ``` ### EdenQueryKeyMeta [#edenquerykeymeta] Metadata included in query keys. ```ts type EdenQueryKeyMeta = { input?: TInput pathParams?: EdenQueryKeyPathParam[] type?: 'query' | 'infinite' infinite?: { initialPageParam?: unknown } } ``` ### EdenQueryKeyPathParam [#edenquerykeypathparam] An ordered path-parameter application, exported by `eden-tanstack-react-query`. `pathIndex` is the zero-based index of the path segment where the parameters were applied. It is `-1` for parameters applied directly to the root proxy. `entries` records the parameter names and values separately from request input. ```ts interface EdenQueryKeyPathParam { pathIndex: number entries: [name: string, value: unknown][] } ``` ### QueryType [#querytype] Query type discriminator for cache organization. ```ts type QueryType = 'query' | 'infinite' | 'any' ``` ## Type Inference Utilities [#type-inference-utilities] ### Route Type Inference [#route-type-inference] | Type | Description | |------|-------------| | `InferRouteInput` | Input type for route/method | | `InferRouteOutput` | Declared successful response types | | `InferRouteError` | Non-success response and transport error types | ### InferRouteInput [#inferrouteinput] Extract input type from a RouteSchema. ```ts // For GET/HEAD/OPTIONS: returns query parameters type // For POST/PUT/PATCH/DELETE: returns body type type InferRouteInput = TMethod extends 'get' | 'head' | 'options' ? QueryParams : Body ``` ### InferRouteOutput [#inferrouteoutput] Extract the union of declared successful response types from a route. Treaty treats every status from `200` through `299` as successful and returns an empty string for bodyless `204` and `205` responses, regardless of their declared response schema. Response types follow Treaty's response conversions, with the serialization limits described below. Elysia `form()` responses expose their payload object, and `ReadableStream` is typed as `AsyncGenerator`. Sync and async generators that yield no values and return an object resolve to that object type. Generators that can both yield and return a value include the async generator and return value in their union. The streamed branch completes with `void`; its server-side return value is not delivered. An early void return decodes to an empty string, so generators that can yield and return void include `""` in their output union. When updating existing code, read form payload fields directly and consume streams with `for await...of`. A generator response may need narrowing before iteration. #### Multipart and stream decoding limits [#multipart-and-stream-decoding-limits] These types inherit limitations from native Treaty. Multipart fields and stream chunks can decode to values that differ from their inferred types. For multipart responses, `form()` preserves the original payload field types in inference, but non-file fields are serialized as strings. For example, `form({ age: 42, active: true })` decodes to `{ age: '42', active: 'true' }`, although the fields are still typed as `number` and `boolean`. Prefer string and `File` fields in multipart responses, and explicitly parse and validate fields that need other types. For streams, the inferred yield type retains the original chunk type, but Treaty can decode bytes to text and parse text as JSON. A `ReadableStream` containing encoded `hello` can yield a string, and a `ReadableStream` containing `42` can yield the number `42`. Decoding depends on content type, parser configuration, and wire boundaries. When the decoded shape is uncertain, pass each value to a validator that accepts `unknown` before using methods such as `subarray()` or relying on a particular object shape. ### InferRouteOutputAll [#inferrouteoutputall] Extract the response record for every declared status, with the same response conversions and decoding limits described above. This utility preserves status keys and declared `204`/`205` values; `InferRouteOutput` applies the bodyless-status override. ### InferRouteError [#inferrouteerror] Extract the union of non-success response and transport error types from a route. Declared error `value` types use the same form, stream, and generator conversions and decoding limits as successful responses. The union also includes Treaty's `EdenFetchError<503, unknown>` transport failure because a custom fetcher can reject with any JavaScript value. Routes without a declared non-success response fall back to `EdenFetchError`. A declared HTTP `503` response remains in the union, but narrowing only by status leaves `value` unknown because the same status can represent a transport failure. ### InferRouteBody [#inferroutebody] Extract body type from a route. ### InferRouteQuery [#inferroutequery] Extract query parameters type from a route. ### InferRouteParams [#inferrouteparams] Extract path parameters type from a route. ### InferRouteHeaders [#inferrouteheaders] Extract headers type from a route. ### ExtractRoutes [#extractroutes] Extract routes schema from an Elysia app type. ```ts type ExtractRoutes = TApp['~Routes'] ``` ### GetRoute [#getroute] Get a specific route by path and method. ```ts type UserRoute = GetRoute ``` ### ExtractPathParams [#extractpathparams] Extract path parameter names from a route path string. ```ts type Params = ExtractPathParams<'/users/:id/posts/:postId'> // => 'id' | 'postId' ``` ### PathParamsToObject [#pathparamstoobject] Create params object type from path string. ```ts type Params = PathParamsToObject<'/users/:id/posts/:postId'> // => { id: string; postId: string } ``` ### IsQueryMethod / IsMutationMethod [#isquerymethod--ismutationmethod] Type guards for method categorization. ## Decorator Types [#decorator-types] ### EdenOptionsProxy [#edenoptionsproxy] Full decorated options proxy type for an Elysia app. ```ts // Usage type Proxy = EdenOptionsProxy // Proxy.users.get.queryOptions(...) // Proxy.users({ id: '1' }).get.queryOptions(...) // Proxy.users.post.mutationOptions(...) ``` ### DecorateQueryProcedure [#decoratequeryprocedure] Decorator for query procedures (GET, OPTIONS, HEAD). ### DecorateMutationProcedure [#decoratemutationprocedure] Decorator for mutation procedures (POST, PUT, PATCH, DELETE). ### DecorateInfiniteQueryProcedure [#decorateinfinitequeryprocedure] Decorator for infinite query procedures. ### DecorateRoute [#decorateroute] Decorate a route based on HTTP method. ### DecoratedRouteMethods [#decoratedroutemethods] Decorate all methods of a route path. ### DecorateRoutes [#decorateroutes] Recursively decorate all routes in an app's route tree. ## Procedure Type Inference [#procedure-type-inference] | Type | Description | |------|-------------| | `inferInput` | Extract input type from decorated procedure | | `inferOutput` | Extract output type from decorated procedure | | `inferError` | Extract error type from decorated procedure | ### inferInput [#inferinput] Infer input type from a decorated procedure. ```ts import type { inferInput } from 'eden-tanstack-react-query' type UserInput = inferInput ``` ### inferOutput [#inferoutput] Infer output type from a decorated procedure. ```ts import type { inferOutput } from 'eden-tanstack-react-query' type UserOutput = inferOutput ``` ### inferError [#infererror] Infer error type from a decorated procedure. ```ts import type { inferError } from 'eden-tanstack-react-query' type UserError = inferError ``` ## Options Types [#options-types] ### EdenQueryOptions [#edenqueryoptions] Query options function type. ### EdenMutationOptions [#edenmutationoptions] Mutation options function type. ### EdenInfiniteQueryOptions [#edeninfinitequeryoptions] Infinite query options function type. ### EdenQueryBaseOptions [#edenquerybaseoptions] Base options for Eden requests. ```ts interface EdenQueryBaseOptions { eden?: { /** Abort request on component unmount */ abortOnUnmount?: boolean } } ``` ### EdenQueryOptionsResult [#edenqueryoptionsresult] Result metadata added to query options. ```ts interface EdenQueryOptionsResult { eden: { path: string } } ``` ## Cursor Types [#cursor-types] ### ExtractCursorType [#extractcursortype] Extract cursor type from input that has a cursor property. ```ts type Cursor = ExtractCursorType<{ cursor?: string; limit: number }> // => string ``` ### HasCursorInput [#hascursorinput] Check whether a non-union input has a usable required or optional top-level `cursor` key. `any`, `never`, union inputs, and required null-only cursors return `false`. ```ts type HasCursor = HasCursorInput<{ cursor?: string }> // true type HasRequiredCursor = HasCursorInput<{ cursor: string }> // true type NullOnlyCursor = HasCursorInput<{ cursor: null }> // false type NoCursor = HasCursorInput<{ limit: number }> // false ``` ## Utility Types [#utility-types] | Type | Description | |------|-------------| | `DeepPartial` | Makes all properties optional recursively | | `Simplify` | Expands/simplifies complex types | | `IsAny` | Check if type is `any` | | `IsNever` | Check if type is `never` | ### DeepPartial [#deeppartial] Make all properties optional deeply. ```ts type Partial = DeepPartial<{ user: { name: string; email: string } }> // => { user?: { name?: string; email?: string } } ``` ### Simplify [#simplify] Simplify a type by expanding it (improves IDE display). ### IsAny [#isany] Check if type is `any`. ### IsNever [#isnever] Check if type is `never`. ### IsUnknown [#isunknown] Check if type is `unknown`. ## See Also [#see-also] - [Query Keys](/docs/guides/query-keys) - Query key utilities - [queryOptions](/docs/api-reference/use-eden#queryoptions) - Query options factory - [mutationOptions](/docs/api-reference/use-eden#mutationoptions) - Mutation options factory # useEden (/docs/api-reference/use-eden) # useEden [#useeden] The `useEden` hook returns an options proxy that mirrors your Elysia API structure. Each route is decorated with TanStack Query methods. ```ts function useEden(): EdenOptionsProxy ``` Returns an `EdenOptionsProxy` — a proxy object that mirrors your API routes, handles path parameters via function calls, and decorates each endpoint with query/mutation methods. ## Route Methods [#route-methods] ### Query Routes (GET, HEAD, OPTIONS) [#query-routes-get-head-options] | Method | Description | |--------|-------------| | [`queryOptions`](#queryoptions) | Creates options for `useQuery` / `useSuspenseQuery` | | [`queryKey`](#querykey) | Generates a query key for cache operations | | [`queryFilter`](#queryfilter) | Creates a filter for `invalidateQueries`, `cancelQueries`, etc. | | [`infiniteQueryOptions`](#infinitequeryoptions) | Creates options for `useInfiniteQuery` on supported non-union cursor routes | | [`infiniteQueryKey`](#infinitequerykey) | Generates an infinite query key on supported non-union cursor routes | | [`infiniteQueryFilter`](#infinitequeryfilter) | Creates an infinite-query filter on supported non-union cursor routes | ### Mutation Routes (POST, PUT, PATCH, DELETE) [#mutation-routes-post-put-patch-delete] | Method | Description | |--------|-------------| | [`mutationOptions`](#mutationoptions) | Creates options for `useMutation` | | [`mutationKey`](#mutationkey) | Generates a mutation key | --- ## queryOptions [#queryoptions] Creates type-safe options for `useQuery`, `useSuspenseQuery`, `prefetchQuery`, and other TanStack Query functions. ```ts eden.route.get.queryOptions(input, opts?): QueryOptions ``` ### Parameters [#parameters] | Parameter | Type | Description | |-----------|------|-------------| | `input` | `TInput \| SkipToken` | Query parameters or `skipToken` to disable. Optional only when the route query has no required fields | | `opts` | `EdenQueryOptionsIn` | TanStack Query options (excluding reserved) | ### Returns [#returns] | Property | Type | Description | |----------|------|-------------| | `queryKey` | `DataTag` | Unique key based on path and input | | `queryFn` | `QueryFunction` | Fetch function via Eden client | | `eden` | `{ path: string }` | Route metadata | ### Reserved Options [#reserved-options] Automatically set and cannot be overridden: `queryKey`, `queryFn`, `queryHash`. You can configure `queryKeyHashFn`. ### Eden-specific Options [#eden-specific-options] | Option | Type | Description | |--------|------|-------------| | `eden.abortOnUnmount` | `boolean` | Abort request when component unmounts | ### Example [#example] ```tsx const eden = useEden() // Basic const { data } = useQuery(eden.users.get.queryOptions()) // With query params and TQ options const { data } = useQuery( eden.users.get.queryOptions( { role: 'admin' }, { staleTime: 5 * 60 * 1000 } ) ) // Path params const { data } = useQuery( eden.users({ id: '123' }).get.queryOptions() ) // Conditional with skipToken const { data } = useQuery( eden.users({ id: userId ?? '' }).get.queryOptions( userId ? undefined : skipToken ) ) ``` --- ## mutationOptions [#mutationoptions] Creates type-safe options for `useMutation`. ```ts eden.route.post.mutationOptions(opts?): MutationOptions eden.route.post.mutationOptions({ request: true, ...opts }): MutationOptions ``` ### Parameters [#parameters-1] | Parameter | Type | Description | |-----------|------|-------------| | `opts` | `EdenMutationOptionsIn` | TanStack Query mutation options, excluding reserved options, plus the proxy-only `request` flag | | `opts.request` | `true \| false` | Defaults to `false`. Set to `true` for typed `{ body, query, headers }` mutation variables | With `request: true`, each invocation supplies its own body, query values, and headers. Required route fields remain required, and lifecycle callbacks receive the complete variables object. Omitted or false `request` keeps body-only variables. See [per-invocation query and headers](/docs/guides/mutations#per-invocation-query-and-headers) for examples and omission rules. ### Returns [#returns-1] | Property | Type | Description | |----------|------|-------------| | `mutationKey` | `EdenMutationKey` | Unique key based on route path | | `mutationFn` | `MutationFunction` | Mutation function via Eden client | | `eden` | `{ path: string }` | Route metadata | ### Reserved Options [#reserved-options-1] Automatically set: `mutationKey`, `mutationFn`. Mutation inputs do not accept `eden.context` or `eden.abortOnUnmount`. Earlier types accepted these settings, but neither had any effect. Remove them from existing calls. TanStack Query lifecycle callbacks, the context returned by `onMutate`, and `meta` remain supported. The returned `eden` property contains route metadata. ### Example [#example-1] ```tsx const eden = useEden() const queryClient = useQueryClient() const createUser = useMutation({ ...eden.users.post.mutationOptions(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: eden.users.get.queryKey() }) } }) createUser.mutate({ name: 'Alice', email: 'alice@example.com' }) ``` --- ## infiniteQueryOptions [#infinitequeryoptions] Creates type-safe options for `useInfiniteQuery` with cursor-based pagination. This method and the related key and filter helpers are available only when the route declares a usable top-level `cursor` query field on a non-union query schema. A required null-only or `any` cursor is not usable. ```ts eden.route.get.infiniteQueryOptions(input, opts): InfiniteQueryOptions ``` ### Parameters [#parameters-2] | Parameter | Type | Description | |-----------|------|-------------| | `input` | `TInput \| SkipToken` | Query params (excluding cursor) or `skipToken` | | `opts.getNextPageParam` | `(lastPage) => TCursor \| undefined` | **Required.** Returns next page cursor | | `opts.initialCursor` | `NonNullable \| null` | Defaults to `null` for optional cursor fields; required cursor fields need an explicit non-null value | | `opts.getPreviousPageParam` | `(firstPage) => TCursor \| undefined` | For bi-directional pagination | | `opts.maxPages` | `number` | Max pages to keep in memory | ### Example [#example-2] ```tsx const eden = useEden() const { data, fetchNextPage, hasNextPage } = useInfiniteQuery( eden.posts.get.infiniteQueryOptions( { limit: 10 }, { getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, initialCursor: null, } ) ) ``` --- ## queryKey [#querykey] Generates a typed query key for cache operations. ```ts eden.route.get.queryKey(input?): DataTag ``` Key structure: ```ts eden.users.get.queryKey() // => [['users', 'get'], { type: 'query' }] eden.users.get.queryKey({ role: 'admin' }) // => [['users', 'get'], { input: { role: 'admin' }, type: 'query' }] eden.users({ id: '1' }).get.queryKey() // => [['users', 'get'], { // pathParams: [{ pathIndex: 0, entries: [['id', '1']] }], // type: 'query' // }] ``` --- ## mutationKey [#mutationkey] Generates a mutation key from route path. ```ts eden.route.post.mutationKey(): EdenMutationKey ``` ```ts eden.users.post.mutationKey() // => [['users', 'post']] ``` --- ## infiniteQueryKey [#infinitequerykey] Generates an infinite query key (separate from regular query cache). ```ts eden.route.get.infiniteQueryKey( input?, { initialCursor?: TCursor | null }? ): DataTag ``` ```ts eden.posts.get.infiniteQueryKey() // => [['posts', 'get'], { // type: 'infinite', // infinite: { initialPageParam: null } // }] eden.posts.get.infiniteQueryKey({ category: 'tech' }) eden.posts.get.infiniteQueryKey( { category: 'tech' }, { initialCursor: 'start' } ) ``` The initial cursor is part of the exact infinite-query key. Pass the same `initialCursor` used by `infiniteQueryOptions`. Omission targets the default `null` cursor for optional cursor routes; required cursor routes must provide a non-null value. --- ## queryFilter [#queryfilter] Creates a query filter for bulk cache operations like `invalidateQueries`, `cancelQueries`, `removeQueries`. By default, the filter matches both regular and infinite queries. With `exact: true`, it matches the regular query with the same input and path parameters. Use `infiniteQueryFilter` for exact infinite-query matching. ```ts eden.route.get.queryFilter(input?, filters?): QueryFilters ``` | Parameter | Type | Description | |-----------|------|-------------| | `input` | `TInput` | Optional input to narrow the filter | | `filters` | `QueryFilterOptions` | TanStack Query filter options (`exact`, `type`, `stale`, `fetchStatus`, `predicate`) | ```tsx // Invalidate all user queries queryClient.invalidateQueries(eden.users.get.queryFilter()) // Invalidate with specific input queryClient.invalidateQueries( eden.users.get.queryFilter({ role: 'admin' }, { exact: false }) ) ``` --- ## infiniteQueryFilter [#infinitequeryfilter] Same as `queryFilter` but scoped to infinite queries. ```ts eden.route.get.infiniteQueryFilter( input?, filters? ): QueryFilters ``` ```tsx queryClient.invalidateQueries(eden.posts.get.infiniteQueryFilter()) queryClient.invalidateQueries( eden.posts.get.infiniteQueryFilter( { category: 'tech' }, { initialCursor: 'start' } ) ) ``` The filter matches every initial-cursor variant for the selected route input by default. Pass `initialCursor` to target one cursor identity. With the default hash, object and array cursors are complete values rather than partial prefixes; a custom `queryKeyHashFn` defines identity exactly as it does for the cache. Route input still uses TanStack Query's normal partial matching. With `exact: true`, an optional cursor route may omit `initialCursor` to target its default `null` variant. A required cursor route may omit `initialCursor` only for a broad filter with `exact` absent or `false`; exact and cursor-specific filters require an explicit non-null cursor. --- ## Low-level APIs [#low-level-apis] For use outside React context (loaders, server components, utilities): ```ts import { edenQueryOptions, edenMutationOptions, edenInfiniteQueryOptions, } from 'eden-tanstack-react-query' // edenQueryOptions const opts = edenQueryOptions({ path: ['users', 'get'], input: { role: 'admin' }, fetch: async (input, signal) => { /* ... */ }, opts: { staleTime: 5000 } }) // edenMutationOptions const opts = edenMutationOptions({ path: ['users', 'post'], mutate: async (input) => { /* ... */ }, }) // edenInfiniteQueryOptions const opts = edenInfiniteQueryOptions({ path: ['posts', 'get'], input: { limit: 10 }, initialPageParam: null as string | null, fetch: async (inputWithCursor, signal) => { /* ... */ }, opts: { getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined } }) ``` --- ## Types [#types] ### DecorateQueryProcedure [#decoratequeryprocedure] Methods on GET/HEAD/OPTIONS routes: | Method | Type | Description | |--------|------|-------------| | `queryOptions` | `(input, opts?) => EdenQueryOptions` | Options for useQuery; input is conditional on the route query | | `queryKey` | `(input?) => DataTag` | Query key for cache ops | | `queryFilter` | `(input?, filters?) => QueryFilters` | Filter for invalidation | ### DecorateInfiniteQueryProcedure [#decorateinfinitequeryprocedure] Methods on non-union query routes with a usable required or optional top-level `cursor` input: | Method | Type | Description | |--------|------|-------------| | `infiniteQueryOptions` | `(input, opts) => EdenInfiniteQueryOptions` | Options for useInfiniteQuery | | `infiniteQueryKey` | `(input?, keyOpts?) => DataTag` | Infinite query key; required cursors need explicit key options | | `infiniteQueryFilter` | `(input?, filters?) => QueryFilters` | Broad or cursor-specific filter; required cursors need an explicit value for exact matching | ### DecorateMutationProcedure [#decoratemutationprocedure] Methods on POST/PUT/PATCH/DELETE routes: | Method | Type | Description | |--------|------|-------------| | `mutationOptions` | `(opts?) => EdenMutationOptions` | Options for useMutation | | `mutationKey` | `() => EdenMutationKey` | Mutation key | # Comparisons (/docs/comparisons) # Comparisons [#comparisons] ## Quick Comparison [#quick-comparison] | Feature | eden-tanstack-query | @ap0nia/eden-react-query | tRPC v11 | |---------|---------------------|--------------------------|----------| | Backend | Elysia | Elysia | Any (adapters) | | API Style | Options-based | Wrapper hooks + cache utilities | Options-based (`@trpc/tanstack-react-query`) | | Transport | Plain REST (HTTP methods, URLs) | REST + optional batching | RPC protocol | | Query Keys | Explicit utilities | tRPC-style utils | Explicit utilities | | TQ Integration | Native hooks | Wrapper hooks | Native hooks | | Learning Curve | Standard TanStack Query | tRPC-style API | Standard TanStack Query | --- ## vs @ap0nia/eden-react-query [#vs-ap0niaeden-react-query] ### API Comparison [#api-comparison] **Basic Query** ```tsx title="eden-tanstack-query" const eden = useEden() const { data } = useQuery(eden.users.get.queryOptions()) ``` ```tsx title="@ap0nia/eden-react-query" const { data } = eden.users.get.useQuery() ``` **Mutations** ```tsx title="eden-tanstack-query" const createUser = useMutation({ ...eden.users.post.mutationOptions(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: eden.users.get.queryKey() }) } }) ``` ```tsx title="@ap0nia/eden-react-query" const utils = eden.useUtils() const createUser = eden.users.post.useMutation({ onSuccess: () => utils.users.get.invalidate() }) ``` ### Key Differences [#key-differences] **Options-based vs wrapper hooks** — eden-tanstack-query generates options objects that work with any TanStack Query hook (`useQuery`, `useSuspenseQuery`, `prefetchQuery`, `fetchQuery`). eden-react-query provides tRPC-style wrapper hooks on the route proxy and cache utilities through `useUtils()`. **Explicit query keys** — eden-tanstack-query exposes `.queryKey()`, `.queryFilter()`, and `.infiniteQueryFilter()` for full control over cache invalidation. eden-react-query manages keys through tRPC-style utils. **Standard patterns** — If you know TanStack Query, you already know eden-tanstack-query. No custom API to learn. ### When to Choose [#when-to-choose] | Choose eden-tanstack-query | Choose @ap0nia/eden-react-query | |---|---| | Standard TanStack Query patterns | Prefer tRPC-style wrapper hooks | | Explicit cache control needed | Utils-based invalidation feels natural | | Complex invalidation patterns | Coming from tRPC and want that API | | Already familiar with TanStack Query | Batching support wanted | ### Migration from @ap0nia/eden-react-query [#migration-from-ap0niaeden-react-query] **1. Update imports** ```tsx // Before import { createEdenTreatyReactQuery } from '@ap0nia/eden-react-query' const eden = createEdenTreatyReactQuery() // After import { createEdenTanStackQuery } from 'eden-tanstack-react-query' import { treaty } from '@elysiajs/eden' ``` **2. Setup provider** ```tsx const { EdenProvider, useEden } = createEdenTanStackQuery() const edenClient = treaty('http://localhost:3000') ``` **3. Update queries** ```tsx // Before: eden.users.get.useQuery() // After: const eden = useEden() const { data } = useQuery(eden.users.get.queryOptions()) ``` **4. Update mutations** ```tsx // Before: eden.users.post.useMutation() // After: const mutation = useMutation(eden.users.post.mutationOptions()) ``` **5. Update cache operations** ```tsx // Before: const utils = eden.useUtils() utils.users.get.invalidate() // After: queryClient.invalidateQueries(eden.users.get.queryFilter()) ``` --- ## vs tRPC [#vs-trpc] ### Code Comparison [#code-comparison] **Server** ```ts title="Elysia" const app = new Elysia() .get('/users', () => db.users.findMany()) .get('/users/:id', ({ params }) => db.users.findUnique({ where: { id: params.id } })) .post('/users', ({ body }) => db.users.create({ data: body }), { body: t.Object({ name: t.String(), email: t.String({ format: 'email' }) }) }) export type App = typeof app ``` ```ts title="tRPC" const appRouter = t.router({ users: t.router({ list: t.procedure.query(() => db.users.findMany()), get: t.procedure .input(z.object({ id: z.string() })) .query(({ input }) => db.users.findUnique({ where: { id: input.id } })), create: t.procedure .input(z.object({ name: z.string(), email: z.string().email() })) .mutation(({ input }) => db.users.create({ data: input })) }) }) export type AppRouter = typeof appRouter ``` **Client** ```tsx title="eden-tanstack-query" const eden = useEden() const { data } = useQuery(eden.users.get.queryOptions()) const createUser = useMutation({ ...eden.users.post.mutationOptions(), onSuccess: () => queryClient.invalidateQueries(eden.users.get.queryFilter()) }) ``` ```tsx title="tRPC (@trpc/tanstack-react-query)" const trpc = useTRPC() const queryClient = useQueryClient() const { data } = useQuery(trpc.users.list.queryOptions()) const createUser = useMutation(trpc.users.create.mutationOptions({ onSuccess: () => queryClient.invalidateQueries(trpc.users.list.queryFilter()) })) ``` Since tRPC v11, `@trpc/tanstack-react-query` offers the same options-based surface as this library (`queryOptions`, `queryKey`, `queryFilter`, `infiniteQueryOptions`, `mutationOptions`, `mutationKey`). The differences that remain are about the backend and the transport, not the client API style. ### Key Differences [#key-differences-1] | Aspect | eden-tanstack-query | tRPC v11 | |--------|---------------------|----------| | Backend | Elysia (Bun) | Any (Node.js, Bun, etc.) | | API style | REST (HTTP methods, URL paths) | RPC (procedures in routers) | | Client surface | Options factories + explicit keys | Options factories + explicit keys | | Transport | Plain HTTP requests, no codec | tRPC link protocol, optional batching | | Ecosystem | Elysia plugins | Large tRPC ecosystem | | Tooling | Standard REST tools (curl, Postman) | Requires tRPC-aware clients | ### When to Choose [#when-to-choose-1] | Choose eden-tanstack-query | Choose tRPC | |---|---| | Using Elysia + Bun | Need backend framework flexibility | | Prefer REST conventions | Need request batching | | Types flow from real HTTP routes | Existing tRPC codebase | | Standard HTTP tooling needed | Need the larger ecosystem | # Getting Started (/docs/getting-started) # Getting Started [#getting-started] Eden TanStack Query brings end-to-end type safety to your React applications using [Elysia](https://elysiajs.com/) and [TanStack Query](https://tanstack.com/query). ## Installation [#installation] Requires `@tanstack/react-query` 5.102.8 or newer within v5. The peer range is `^5.102.8`. Install the package, its peer dependencies, and the CORS plugin used in this guide: ```bash bun add eden-tanstack-react-query @tanstack/react-query @elysiajs/eden elysia @elysiajs/cors ``` ```bash npm install eden-tanstack-react-query @tanstack/react-query @elysiajs/eden elysia @elysiajs/cors ``` ```bash pnpm add eden-tanstack-react-query @tanstack/react-query @elysiajs/eden elysia @elysiajs/cors ``` ```bash yarn add eden-tanstack-react-query @tanstack/react-query @elysiajs/eden elysia @elysiajs/cors ``` The `elysia` package is only needed for its type definitions. You don't need to run Elysia on the client — just import the `typeof app` type from your server. Ensure your `tsconfig.json` has `strict: true` and `moduleResolution` set to `"bundler"`. ## Upgrading from an earlier release [#upgrading-from-an-earlier-release] Version 0.5.0 raises the React Query minimum from 5.62.0 to 5.102.8. This narrows the supported peer range and is a breaking dependency requirement for applications using an older version. Upgrade `@tanstack/react-query` to 5.102.8 or a later v5 release before upgrading Eden TanStack Query. If you use Query devtools, choose a version whose React Query peer range includes your installed version. The public filter types and mutation function signature require the newer Query types. Version 0.5.0 also tightens query-input and cursor typing; see the [changelog](https://github.com/xkelxmc/eden-tanstack-query/blob/main/packages/eden-tanstack-query/CHANGELOG.md) for those migration steps. ## Define Your Elysia Server [#define-your-elysia-server] Create an Elysia server with typed routes. These route definitions are what provide end-to-end type safety. ```ts title="server/index.ts" import { Elysia, t } from 'elysia' import { cors } from '@elysiajs/cors' const app = new Elysia() .use(cors()) .get('/hello', () => ({ message: 'Hello from Elysia!' })) .get('/users', () => [ { id: '1', name: 'Alice', email: 'alice@example.com' }, { id: '2', name: 'Bob', email: 'bob@example.com' }, ]) .get('/users/:id', ({ params }) => ({ id: params.id, name: `User ${params.id}`, email: `user${params.id}@example.com`, })) .post( '/users', ({ body }) => ({ id: String(Date.now()), ...body }), { body: t.Object({ name: t.String(), email: t.String(), }), } ) .listen(3000) // This type export is what enables type inference on the client export type App = typeof app ``` The `export type App = typeof app` line is crucial — it's what makes the entire type-safe chain work. ## Create Eden Instance [#create-eden-instance] Set up the Eden TanStack Query instance and treaty client. ```ts title="src/lib/eden.ts" import { createEdenTanStackQuery } from 'eden-tanstack-react-query' import { treaty } from '@elysiajs/eden' import type { App } from '../../server' export const { EdenProvider, useEden } = createEdenTanStackQuery() export const edenClient = treaty('http://localhost:3000') ``` ## Set Up Providers [#set-up-providers] Wrap your app with `QueryClientProvider` and `EdenProvider`. ```tsx title="src/App.tsx" import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { EdenProvider, edenClient } from './lib/eden' const queryClient = new QueryClient() export default function App() { return ( ) } ``` ## Use in Components [#use-in-components] ### Query [#query] ```tsx title="src/components/UserList.tsx" import { useQuery } from '@tanstack/react-query' import { useEden } from '../lib/eden' export function UserList() { const eden = useEden() const { data: users, isLoading } = useQuery( eden.users.get.queryOptions() ) if (isLoading) return
Loading...
return (
    {users?.map((user) => (
  • {user.name} ({user.email})
  • ))}
) } ``` ### Path Parameters [#path-parameters] For routes like `/users/:id`, call the path segment as a function: ```tsx const { data: user } = useQuery( eden.users({ id: userId }).get.queryOptions() ) ``` ### Mutation [#mutation] ```tsx title="src/components/CreateUser.tsx" import { useMutation, useQueryClient } from '@tanstack/react-query' import { useEden } from '../lib/eden' export function CreateUser() { const eden = useEden() const queryClient = useQueryClient() const createUser = useMutation({ ...eden.users.post.mutationOptions(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: eden.users.get.queryKey(), }) }, }) return ( ) } ``` ## Monorepo Setup [#monorepo-setup] If your Elysia server and React client live in separate packages, export the app type from your server package and import it in the client: ```ts title="packages/server/src/index.ts" export type App = typeof app ``` ```ts title="packages/client/src/lib/eden.ts" import type { App } from '@myorg/server' ``` ## Next Steps [#next-steps] - **[Query Options](/docs/api-reference/use-eden)** — All available methods and options - **[Infinite Queries](/docs/guides/infinite-queries)** — Pagination with `infiniteQueryOptions()` - **[Query Keys](/docs/guides/query-keys)** — How query keys are generated and used # Overview (/docs/guides) # Guides [#guides] These guides cover the core patterns for using eden-tanstack-query in your applications. Fetch data with type-safe query options Create, update, and delete data with mutations Cursor-based pagination with infinite queries Working with dynamic route segments Cache operations, invalidation, and key structure Server-side rendering with Next.js and TanStack Start Testing patterns with Vitest and Testing Library ## HTTP Methods to Procedures [#http-methods-to-procedures] | HTTP Method | Procedure Type | Options Method | |-------------|----------------|----------------| | GET | Query | `queryOptions()` | | HEAD | Query | `queryOptions()` | | OPTIONS | Query | `queryOptions()` | | POST | Mutation | `mutationOptions()` | | PUT | Mutation | `mutationOptions()` | | PATCH | Mutation | `mutationOptions()` | | DELETE | Mutation | `mutationOptions()` | # Infinite Queries (/docs/guides/infinite-queries) # Infinite Queries [#infinite-queries] Use `infiniteQueryOptions()` for cursor-based pagination with TanStack Query's `useInfiniteQuery`. The infinite-query methods are exposed only when the route declares a required or optional usable top-level `cursor` query field on a non-union query schema. Union query schemas are excluded because TypeScript cannot safely correlate a variant's remaining input with its cursor type through this API. ## Server Route [#server-route] A typical cursor-based pagination endpoint: ```ts app.get('/posts', ({ query }) => { const { limit, cursor } = query const posts = getPosts({ limit, after: cursor }) return { items: posts, nextCursor: posts.length === limit ? posts[posts.length - 1].id : null, } }, { query: t.Object({ limit: t.Number({ default: 10 }), cursor: t.Optional(t.String()), }) }) ``` ## Basic Usage [#basic-usage] ```tsx import { useInfiniteQuery } from '@tanstack/react-query' import { useEden } from './lib/eden' function PostList() { const eden = useEden() const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, } = useInfiniteQuery( eden.posts.get.infiniteQueryOptions( { limit: 10 }, { getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, initialCursor: null, } ) ) if (isLoading) return
Loading...
return (
{data?.pages.map((page) => page.items.map((post) => (

{post.title}

)) )}
) } ``` ## Pagination Options [#pagination-options] `getNextPageParam` is required when using `infiniteQueryOptions()`: **getNextPageParam** -- determines the cursor for the next page. Return `undefined` to indicate there are no more pages: ```tsx { getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined } ``` For an optional cursor field, `initialCursor` defaults to `null`. Treaty omits that null query value, which represents an absent optional cursor: ```tsx { initialCursor: null // or 0, '', depending on your API } ``` For a required cursor field, provide an explicit non-null value: ```tsx eden.posts.get.infiniteQueryOptions( { limit: 10 }, { initialCursor: 'start', getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, } ) ``` This also applies when the required cursor schema includes `null`: Treaty omits null query values, so null cannot satisfy a required query property on the wire. ## Input Parameters [#input-parameters] The first argument is your query input (excluding cursor). The library automatically adds the cursor to each page request: ```tsx eden.posts.get.infiniteQueryOptions( { limit: 10, category: 'technology' }, { getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, initialCursor: null, } ) ``` ## Path Parameters [#path-parameters] Combine path parameters with pagination: ```tsx // GET /users/:id/posts function UserPosts({ userId }: { userId: string }) { const eden = useEden() const { data } = useInfiniteQuery( eden.users({ id: userId }).posts.get.infiniteQueryOptions( { limit: 10 }, { getNextPageParam: (page) => page.nextCursor ?? undefined, initialCursor: null, } ) ) } ``` ## Infinite Query Key and Filter [#infinite-query-key-and-filter] Use `infiniteQueryKey()` and `infiniteQueryFilter()` for cache operations: ```tsx const queryClient = useQueryClient() const eden = useEden() // Get the infinite query key const key = eden.posts.get.infiniteQueryKey({ limit: 10 }) // Match options that start from a non-default cursor const resumedKey = eden.posts.get.infiniteQueryKey( { limit: 10 }, { initialCursor: 'cursor-20' } ) // Invalidate infinite query queryClient.invalidateQueries({ queryKey: eden.posts.get.infiniteQueryKey() }) // Using infiniteQueryFilter queryClient.invalidateQueries( eden.posts.get.infiniteQueryFilter({ category: 'tech' }) ) // Invalidate only the variant that starts from cursor-20 queryClient.invalidateQueries( eden.posts.get.infiniteQueryFilter( { category: 'tech' }, { initialCursor: 'cursor-20' } ) ) ``` Exact infinite-query keys include the initial cursor. Filters omit it on purpose and match every starting cursor for the selected input unless an `initialCursor` filter option is provided. A provided cursor is compared through the candidate query's key hash, so the default hash treats it as one complete value while a custom `queryKeyHashFn` retains its own cache identity. Route input remains partially matchable. With `exact: true`, omitting `initialCursor` targets only queries whose initial cursor is the default `null` on optional cursor routes. Required cursor routes allow omission only for broad filters with `exact` absent or `false`; pass an explicit non-null `initialCursor` for exact or cursor-specific filtering. ## Conditional with skipToken [#conditional-with-skiptoken] ```tsx import { skipToken } from '@tanstack/react-query' function UserPosts({ userId }: { userId: string | null }) { const eden = useEden() const { data } = useInfiniteQuery( eden.users({ id: userId ?? '' }).posts.get.infiniteQueryOptions( userId ? { limit: 10 } : skipToken, { getNextPageParam: (page) => page.nextCursor ?? undefined, initialCursor: null, } ) ) } ``` ## Abort on Unmount [#abort-on-unmount] ```tsx const { data } = useInfiniteQuery( eden.posts.get.infiniteQueryOptions( { limit: 10 }, { getNextPageParam: (page) => page.nextCursor ?? undefined, initialCursor: null, eden: { abortOnUnmount: true }, } ) ) ``` # Mutations (/docs/guides/mutations) # Mutations [#mutations] Any route using POST, PUT, PATCH, or DELETE is treated as a mutation procedure. Use `mutationOptions()` to create options for TanStack Query's `useMutation`. ## Basic Mutation [#basic-mutation] ```tsx import { useMutation } from '@tanstack/react-query' import { useEden } from './lib/eden' function CreateUserForm() { const eden = useEden() const createUser = useMutation( eden.users.post.mutationOptions() ) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() const formData = new FormData(e.currentTarget) createUser.mutate({ name: formData.get('name') as string, email: formData.get('email') as string, }) } return (
) } ``` ## Required and optional bodies [#required-and-optional-bodies] A declared object body is required even when every field is optional. For a schema such as `t.Object({ name: t.Optional(t.String()) })`, pass an object: ```tsx const updateUser = useMutation(eden.users.patch.mutationOptions()) updateUser.mutate({}) updateUser.mutate({ name: 'Ada' }) ``` If older code called `mutate()` or `mutateAsync()` for this schema, replace it with `mutate({})` or `mutateAsync({})`. Omitting the body now fails type checking; it previously sent `undefined`, which the server could reject with a validation error. Routes without a body and optional object bodies, such as `t.Optional(t.Object({ name: t.String() }))`, still allow omitted variables. Their lifecycle callbacks must handle missing variables before reading body fields. The standalone `edenMutationOptions` factory allows omission when its input type includes `undefined`. Some Elysia versions encode optional object bodies as `Partial | null`. The proxy also allows omission for this shape. A declared required nullable object with all optional fields has the same type, so the proxy cannot distinguish it from an optional body. Pass an object explicitly for that schema; server handling of an omitted body depends on Elysia. Optional primitive bodies can share their type with required nullable primitives; pass variables explicitly for those schemas. ## Per-invocation query and headers [#per-invocation-query-and-headers] Pass `request: true` to send `{ body, query, headers }` variables. Query values and headers are inferred from the Elysia route, including required fields. For example, this route requires a body, a query parameter, and an idempotency header: ```ts const app = new Elysia().post( '/jobs', ({ body, query }) => ({ name: body.name, dryRun: query.dryRun }), { body: t.Object({ name: t.String() }), query: t.Object({ dryRun: t.Boolean() }), headers: t.Object({ 'idempotency-key': t.String() }), }, ) ``` ```tsx const createJob = useMutation( eden.jobs.post.mutationOptions({ request: true, onMutate: (variables) => ({ key: variables.headers['idempotency-key'] }), onError: (error, variables, context) => { console.error(context?.key, variables.body.name, error) }, }), ) const submission = { body: { name: 'Import contacts' }, query: { dryRun: false }, headers: { 'idempotency-key': crypto.randomUUID() }, } await createJob.mutateAsync(submission) ``` Create the key once per logical submission and reuse `submission` when retrying it. TanStack retries reuse the same variables. The server must implement idempotency handling; the header alone does not prevent duplicate writes. When extracting request options into a variable, preserve the literal `true` type with `satisfies`: ```tsx const requestOptions = { request: true } satisfies { request: true } const createJob = useMutation(eden.jobs.post.mutationOptions(requestOptions)) ``` A plain `const requestOptions = { request: true }` infers a mutable `boolean` field. `mutationOptions` rejects that type because the flag determines whether mutation variables are a body or a request object. For reusable options with callbacks, extract the result of `mutationOptions({ request: true, ... })` so callback types are inferred at that call. Lifecycle callbacks receive the complete variables object, and `onMutate` still infers the context passed to later callbacks. Bind dynamic path parameters before calling `mutationOptions`: ```tsx const deleteUser = useMutation( eden.users({ id: userId }).delete.mutationOptions({ request: true }), ) // A route without a body can still send per-call headers. deleteUser.mutate({ headers: { 'if-match': revision } }) ``` The `body` field is required when the route requires a body, even if every body property is optional. Omit `body` for a bodyless route. You may omit the entire variables argument when the body, query, and headers are all optional. Request mode supports query values and headers; configure other request settings through Treaty. Without `request: true`, variables remain the body. A body containing fields named `body`, `query`, or `headers` is sent unchanged. Both modes use the same mutation key. ## Cache Invalidation [#cache-invalidation] Invalidate related queries after a successful mutation using `queryKey()` or `queryFilter()`: ```tsx import { useQueryClient, useMutation } from '@tanstack/react-query' function CreateUserForm() { const eden = useEden() const queryClient = useQueryClient() const createUser = useMutation({ ...eden.users.post.mutationOptions(), onSuccess: () => { // Using queryKey queryClient.invalidateQueries({ queryKey: eden.users.get.queryKey() }) // Or using queryFilter queryClient.invalidateQueries( eden.users.get.queryFilter() ) }, }) } ``` Invalidate multiple related queries: ```tsx const createPost = useMutation({ ...eden.posts.post.mutationOptions(), onSuccess: (newPost) => { queryClient.invalidateQueries({ queryKey: eden.posts.get.queryKey() }) queryClient.invalidateQueries({ queryKey: eden.users({ id: newPost.authorId }).posts.get.queryKey() }) }, }) ``` ## Optimistic Updates [#optimistic-updates] Update the UI immediately while the mutation runs in the background. Pass the lifecycle callbacks inside `mutationOptions()`. Spreading the options and adding `onMutate` at the `useMutation` call site does not compile: the spread pins the mutation's context type before `onMutate` can infer it, so the `context` argument of `onError` becomes `never`. ```tsx const toggleTodo = useMutation( eden.todos({ id: todo.id }).patch.mutationOptions({ onMutate: async (newTodo) => { await queryClient.cancelQueries({ queryKey: eden.todos.get.queryKey() }) const previousTodos = queryClient.getQueryData(eden.todos.get.queryKey()) queryClient.setQueryData( eden.todos.get.queryKey(), (old: Todo[] | undefined) => old?.map(t => t.id === todo.id ? { ...t, completed: !t.completed } : t) ) return { previousTodos } }, onError: (_err, _newTodo, context) => { queryClient.setQueryData(eden.todos.get.queryKey(), context?.previousTodos) }, onSettled: () => { queryClient.invalidateQueries({ queryKey: eden.todos.get.queryKey() }) }, }) ) ``` ## Mutation Key [#mutation-key] Get the mutation key for tracking mutation state: ```tsx import { useMutationState } from '@tanstack/react-query' const eden = useEden() // Get mutation key const key = eden.users.post.mutationKey() // Track pending mutations const pendingMutations = useMutationState({ filters: { mutationKey: eden.users.post.mutationKey(), status: 'pending', }, }) ``` ## Path Parameters [#path-parameters] Path parameters work the same way as with queries: ```tsx // PUT /users/:id const updateUser = useMutation( eden.users({ id: userId }).put.mutationOptions() ) updateUser.mutate({ name: 'New Name' }) // DELETE /users/:id const deleteUser = useMutation( eden.users({ id: userId }).delete.mutationOptions() ) deleteUser.mutate() ``` ## mutateAsync [#mutateasync] Use `mutateAsync` for promise-based control flow: ```tsx const createUser = useMutation(eden.users.post.mutationOptions()) async function handleSubmit(data: CreateUserInput) { try { const user = await createUser.mutateAsync(data) navigate(`/users/${user.id}`) } catch (error) { console.error('Failed:', error) } } ``` # Path Parameters (/docs/guides/path-parameters) # Path Parameters [#path-parameters] Eden TanStack Query provides a natural, type-safe way to work with dynamic route segments. Path parameters like `/users/:id` are handled through callable functions on the proxy object, maintaining full type inference from your Elysia routes. ## Basic Path Parameters [#basic-path-parameters] When your Elysia route includes path parameters (prefixed with `:`), the Eden proxy becomes callable at that segment: ```ts title="server.ts" import { Elysia, t } from 'elysia' const app = new Elysia() .get('/users/:id', ({ params }) => ({ id: params.id, name: `User ${params.id}`, email: `user${params.id}@example.com` }), { params: t.Object({ id: t.String() }) }) .listen(3000) export type App = typeof app ``` ```tsx title="UserProfile.tsx" import { useQuery } from '@tanstack/react-query' import { useEden } from './lib/eden' function UserProfile({ userId }: { userId: string }) { const eden = useEden() // Call the path segment as a function with params object const { data: user, isPending } = useQuery( eden.users({ id: userId }).get.queryOptions() ) if (isPending) return
Loading...
return (

{user?.name}

{user?.email}

) } ``` The parameter name (`id`) in the object must match the route parameter name (`:id`). TypeScript will enforce this at compile time. ## Optional path parameters [#optional-path-parameters] For `/users/:id?`, use `id` without the `?` suffix when supplying a value. To omit the parameter, access the route directly: ```ts eden.users({ id: '123' }).get.queryOptions() eden.users.get.queryOptions() ``` The callable form still requires a value. Use the direct branch instead of passing `{}` or `undefined`. `ExtractPathParams<'/users/:id?'>` produces `'id'`, and `PathParamsToObject<'/users/:id?'>` produces `{ id?: string }`. ## Multiple Path Parameters [#multiple-path-parameters] Routes with multiple parameters at the same level work the same way: ```ts title="server.ts" const app = new Elysia() .get('/files/:category/:filename', ({ params }) => ({ category: params.category, filename: params.filename, url: `/cdn/${params.category}/${params.filename}` }), { params: t.Object({ category: t.String(), filename: t.String() }) }) ``` ```tsx title="FileViewer.tsx" function FileViewer({ category, filename }: { category: string; filename: string }) { const eden = useEden() // Adjacent params are applied in route order const { data: file } = useQuery( eden.files({ category })({ filename }).get.queryOptions() ) return
{file?.url}
} ``` ## Nested Path Parameters [#nested-path-parameters] For deeply nested routes with parameters at different levels, chain the function calls: ```ts title="server.ts" const app = new Elysia() .get('/posts/:postId/comments/:commentId', ({ params }) => ({ postId: params.postId, commentId: params.commentId, content: 'Comment content here' }), { params: t.Object({ postId: t.String(), commentId: t.String() }) }) ``` ```tsx title="Comment.tsx" function Comment({ postId, commentId }: { postId: string; commentId: string }) { const eden = useEden() // Chain function calls for nested params const { data: comment } = useQuery( eden.posts({ postId }).comments({ commentId }).get.queryOptions() ) return
{comment?.content}
} ``` ## Path Parameters in Query Keys [#path-parameters-in-query-keys] Path parameters are stored separately from request input in the query key. Their path position and application order are retained so query fields, duplicate names, and infinite-query cursors cannot overwrite path identity. ```tsx const eden = useEden() // These produce different query keys: eden.users({ id: '1' }).get.queryKey() // => [['users', 'get'], { // pathParams: [{ pathIndex: 0, entries: [['id', '1']] }], // type: 'query' // }] eden.users({ id: '2' }).get.queryKey() // => [['users', 'get'], { // pathParams: [{ pathIndex: 0, entries: [['id', '2']] }], // type: 'query' // }] ``` This means TanStack Query will correctly cache and refetch data for each unique combination of path parameters. ## Combining Path Parameters with Query Parameters [#combining-path-parameters-with-query-parameters] Path parameters can be combined with query parameters. The path params are passed to the callable function, while query params go to the options method: ```ts title="server.ts" const app = new Elysia() .get('/users/:id/posts', ({ params, query }) => ({ userId: params.id, posts: [], page: query.page ?? 1, limit: query.limit ?? 10 }), { params: t.Object({ id: t.String() }), query: t.Object({ page: t.Optional(t.Number()), limit: t.Optional(t.Number()) }) }) ``` ```tsx title="UserPosts.tsx" function UserPosts({ userId }: { userId: string }) { const eden = useEden() const [page, setPage] = useState(1) const { data } = useQuery( eden.users({ id: userId }).posts.get.queryOptions({ page, limit: 20 }) ) return (
{data?.posts.map(post => ( ))}
) } ``` ## Path Parameters with Mutations [#path-parameters-with-mutations] Path parameters work the same way with mutations: ```ts title="server.ts" const app = new Elysia() .delete('/users/:id', ({ params }) => ({ deleted: true, id: params.id }), { params: t.Object({ id: t.String() }) }) .patch('/users/:id', ({ params, body }) => ({ id: params.id, name: body.name }), { params: t.Object({ id: t.String() }), body: t.Object({ name: t.String() }) }) ``` ```tsx title="UserActions.tsx" function UserActions({ userId }: { userId: string }) { const eden = useEden() const queryClient = useQueryClient() const deleteUser = useMutation({ ...eden.users({ id: userId }).delete.mutationOptions(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: eden.users.get.queryKey() }) } }) const updateUser = useMutation({ ...eden.users({ id: userId }).patch.mutationOptions(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: eden.users({ id: userId }).get.queryKey() }) } }) return (
) } ``` ## Type Safety [#type-safety] TypeScript ensures all path parameters are present. If you miss a parameter, you get a compile-time error: ```tsx const eden = useEden() // Error: Property 'id' is missing eden.users({}).get.queryOptions() // Both compile — params accept string | number eden.users({ id: 123 }).get.queryOptions() eden.users({ id: '123' }).get.queryOptions() ``` Parameter names are inferred from the route path. Every parameter accepts `string | number`; values are serialized into the URL, and the library normalizes them when building the query key, so numeric and string forms share one cache entry. ## Method-Name Collisions [#method-name-collisions] A static URL segment named after an HTTP method remains navigable when it is also a procedure, such as `eden.account.delete.post` for `POST /account/delete`. The procedure itself stays a plain object, so path parameters immediately after that method-named segment are unsupported. Procedure and route-schema members, host probes, and inherited `Object` members are also reserved at that position. Use a different route shape or the Treaty client directly for those collisions. Outside a method-procedure collision, host-probe names such as `toJSON` and `$$typeof` are regular path segments. `then` is reserved everywhere so the proxy cannot be mistaken for a promise. ## Cache Invalidation with Path Parameters [#cache-invalidation-with-path-parameters] When invalidating cache for routes with path parameters, you can be as specific or as broad as needed: ```tsx const queryClient = useQueryClient() const eden = useEden() // Invalidate a specific user queryClient.invalidateQueries({ queryKey: eden.users({ id: '1' }).get.queryKey() }) // Invalidate all users queries (using queryFilter without params) queryClient.invalidateQueries(eden.users.get.queryFilter()) // Invalidate specific user's posts queryClient.invalidateQueries({ queryKey: eden.users({ id: '1' }).posts.get.queryKey() }) ``` ## Best Practices [#best-practices] 1. **Keep parameter names consistent** - Use the same parameter names across related routes for clarity. 2. **Validate parameters on the server** - Always define `params` schemas in Elysia for type safety and validation. 3. **Use descriptive parameter names** - Prefer `userId` over `id` in nested routes for clarity. 4. **Consider URL structure** - Design your routes with caching in mind. Related resources should share path prefixes for easy cache invalidation. # Queries (/docs/guides/queries) # Queries [#queries] Any route using GET, HEAD, or OPTIONS is treated as a query procedure. Use `queryOptions()` to create options for TanStack Query's `useQuery`. ## Basic Query [#basic-query] Using the server from the [Getting Started](/docs/getting-started) guide: ```tsx import { useQuery } from '@tanstack/react-query' import { useEden } from './lib/eden' function UserList() { const eden = useEden() const { data, isLoading, error } = useQuery( eden.users.get.queryOptions() ) if (isLoading) return
Loading...
if (error) return
Request failed with status {error.status}
return (
    {data?.map(user => (
  • {user.name}
  • ))}
) } ``` ## Query with Input [#query-with-input] Pass query parameters as the first argument: ```tsx // GET /users?role=admin&status=active const { data } = useQuery( eden.users.get.queryOptions({ role: 'admin', status: 'active' }) ) ``` The input argument is optional only when every query field is optional. A route with required query fields must receive the complete input or `skipToken`. For per-request headers, use a wrapped input: ```tsx eden.users.get.queryOptions({ query: { role: 'admin' }, headers: { authorization: 'Bearer token' }, }) ``` Wrapped input requires an explicit `headers` property. Without headers, pass `{ role: 'admin' }` directly or `{ query: { role: 'admin' }, headers: undefined }`. A lone `{ query: value }` is treated as a query parameter named `query`, not a request wrapper. These rules also apply to `infiniteQueryOptions()`. If your schema has a field named `query`, use an outer wrapper for HTTP headers: ```tsx eden.search.get.queryOptions({ query: { query: 'hello' }, headers: { authorization: 'Bearer token' }, }) ``` The flattened form `{ query: 'hello', headers: { authorization: 'Bearer token' } }` has the same shape as a wrapper around a string query, so it is rejected for this schema. Direct `{ query: 'hello', headers: 'query-value' }` remains valid when both fields are scalar query parameters. If the schema's `headers` field can be `undefined` and it also declares `query`, use a wrapper for all inputs, including inputs that omit `headers`. For a direct `{ query: value }`, omit the transport `headers` property entirely. Without TypeScript's `exactOptionalPropertyTypes`, an optional `never` property still accepts `undefined`, so types cannot reject every explicit `headers: undefined`. At runtime, an object containing only `query` and `headers` is a wrapper when `headers` is `undefined` or a non-null, non-array object. This also applies to untyped inputs and partial inputs passed to key and filter helpers. If an existing wrapper variable declares `headers?`, pass `{ ...input, headers: input.headers }` to make the property explicit. If your query schema declares an object-valued field named `headers`, wrap the query. Direct input would send that field as HTTP headers instead of a query parameter. Migrate existing calls by adding `query` and `headers: undefined`: ```tsx eden.items.get.queryOptions({ query: { headers: { mode: 'compact' } }, headers: undefined, }) ``` This restriction also covers optional fields and fields that allow both object and scalar values. Array and other object types require a wrapper too, even when the runtime would accept them directly. For a query union, members without colliding fields still accept direct input. If any member has a `query` field or an object-valued `headers` field, use a wrapper for HTTP headers on all members. This prevents a variable with ambiguous `headers` from matching a member that does not declare that field. Schemas without a collision continue to accept flattened HTTP headers, such as `{ role: 'admin', headers: { authorization: 'Bearer token' } }`. The same input rules apply to infinite queries. Pass additional TanStack Query options as the second argument: ```tsx const { data } = useQuery( eden.users.get.queryOptions( { role: 'admin' }, { staleTime: 5 * 60 * 1000 } ) ) ``` ## Path Parameters [#path-parameters] For routes with dynamic segments, call the path segment as a function. See the [Path Parameters](/docs/guides/path-parameters) guide for details. ```tsx // GET /users/:id const { data } = useQuery( eden.users({ id: userId }).get.queryOptions() ) ``` ## useSuspenseQuery [#usesuspensequery] Works with `useSuspenseQuery` for React Suspense support: ```tsx import { useSuspenseQuery } from '@tanstack/react-query' function UserProfile({ userId }: { userId: string }) { const eden = useEden() // data is guaranteed to be defined const { data } = useSuspenseQuery( eden.users({ id: userId }).get.queryOptions() ) return
{data.name}
} ``` ## Query Key [#query-key] Use `queryKey()` for cache operations: ```tsx const queryClient = useQueryClient() const eden = useEden() // Invalidate all users queries queryClient.invalidateQueries({ queryKey: eden.users.get.queryKey() }) // Invalidate with specific input queryClient.invalidateQueries({ queryKey: eden.users.get.queryKey({ role: 'admin' }) }) ``` See the [Query Keys](/docs/guides/query-keys) guide for full details on key structure and cache operations. ## Query Filter [#query-filter] Use `queryFilter()` for invalidation with additional filter options: ```tsx queryClient.invalidateQueries( eden.users.get.queryFilter() ) // With input filtering queryClient.invalidateQueries( eden.users.get.queryFilter({ role: 'admin' }) ) // With additional filter options queryClient.invalidateQueries( eden.users.get.queryFilter(undefined, { exact: true, stale: true }) ) ``` ## Conditional Queries with skipToken [#conditional-queries-with-skiptoken] Use `skipToken` to conditionally disable queries: ```tsx import { useQuery, skipToken } from '@tanstack/react-query' function UserProfile({ userId }: { userId: string | null }) { const eden = useEden() const { data } = useQuery( eden.users({ id: userId ?? '' }).get.queryOptions( userId ? undefined : skipToken ) ) if (!userId) return
Select a user
return
{data?.name}
} ``` ## Abort on Unmount [#abort-on-unmount] Enable request cancellation when the component unmounts: ```tsx const { data } = useQuery( eden.users.get.queryOptions( undefined, { eden: { abortOnUnmount: true } } ) ) ``` ## Prefetching [#prefetching] Prefetch data before it's needed: ```tsx const queryClient = useQueryClient() const eden = useEden() const handleMouseEnter = () => { queryClient.prefetchQuery( eden.users({ id: userId }).get.queryOptions() ) } ``` ## Error Handling [#error-handling] Errors are typed as `EdenFetchError`: `status` is the HTTP status code and `value` is the response body. ```tsx const { data, error } = useQuery(eden.users.get.queryOptions()) if (error) { if (error.status === 401) { return
Please log in
} return
Request failed with status {error.status}
} ``` `error.value` is typed as `unknown` unless the route declares its error responses. Declare them in Elysia to get typed narrowing by status: ```ts // server .get('/items/:id', handler, { response: { 200: t.Object({ name: t.String() }), 404: t.Object({ message: t.String() }), }, }) ``` ```tsx // client if (error) { if (error.status === 404) { // error.value is narrowed to { message: string } return
{error.value.message}
} return
Request failed with status {error.status}
} ``` Avoid rendering `error.message`: the error extends `Error`, but its runtime `message` is `String(value)` — for JSON bodies that renders as `[object Object]`. Use `status` and `value` instead. # Query Keys (/docs/guides/query-keys) # Query Keys [#query-keys] Eden TanStack Query generates deterministic query keys from your route paths and input. These keys are used by TanStack Query for caching, deduplication, and invalidation. ## Key Structure [#key-structure] Query keys follow this structure: ```ts // queryKey() returns: [['path', 'segments', 'method'], { input: { ... }, type: 'query' }] // Examples: eden.users.get.queryKey() // => [['users', 'get'], { type: 'query' }] eden.users.get.queryKey({ role: 'admin' }) // => [['users', 'get'], { input: { role: 'admin' }, type: 'query' }] eden.users({ id: '1' }).get.queryKey() // => [['users', 'get'], { // pathParams: [{ pathIndex: 0, entries: [['id', '1']] }], // type: 'query' // }] ``` The `type` field differentiates regular queries from infinite queries: ```ts eden.posts.get.infiniteQueryKey({ limit: 10 }) // => [['posts', 'get'], { // input: { limit: 10 }, // type: 'infinite', // infinite: { initialPageParam: null } // }] ``` ## Cache Identity [#cache-identity] ### Path parameters [#path-parameters] Path parameters are stored separately from request input and retain their path position and application order. A query field cannot overwrite path identity, and sequential parameters with the same name remain distinct. ### Request headers [#request-headers] Per-request headers are included in the `input` part of the query key. Different tenant, authorization, locale, or other response-varying headers therefore create separate cache entries. Header values are visible in TanStack Query Devtools and can be serialized by dehydration or persistence. Treat raw secrets in per-request headers as an explicit cache-identity trade-off, and only use them when that exposure is understood and controlled. ### Wrapped input migration [#wrapped-input-migration] An explicit `{ query: value, headers: undefined }` wrapper now uses `{ query: value, headers: {} }` in its cache key. The request still receives no per-request headers. This separates its cache entry from direct `{ query: value }` input, which sends a query parameter named `query`. All query and infinite-query option, key, and filter helpers use this normalization. When upgrading, discard all old persisted entries for affected route/input combinations, including entries whose keys look like direct input, or reset the persisted cache. Old serialized keys cannot distinguish wrapper requests from direct requests. Regenerate manually stored keys with the helpers. Existing direct-input keys and wrappers with header values are unchanged. Filters retain TanStack Query's partial matching, so a direct-input filter can still match wrappers with the same `query` value, and an empty-headers wrapper filter can match wrappers with additional headers. ### skipToken [#skiptoken] `skipToken` is supported by `queryOptions()` and `infiniteQueryOptions()` to create disabled queries. It is not valid input for key-only helpers: `queryKey(skipToken)` and `infiniteQueryKey(skipToken)` throw an error. ## Available Methods [#available-methods] ### queryKey() [#querykey] Returns the query key for a specific route. Optionally accepts input for more specific targeting: ```ts eden.users.get.queryKey() // all users queries eden.users.get.queryKey({ role: 'admin' }) // specific input eden.users({ id: '1' }).get.queryKey() // specific path params ``` ### queryFilter() [#queryfilter] Returns a filter object (including `queryKey`) for use with TanStack Query's filter-based methods: ```ts eden.users.get.queryFilter() eden.users.get.queryFilter({ role: 'admin' }) eden.users.get.queryFilter(undefined, { exact: true, stale: true }) ``` `queryKey()` generates keys with `type: "query"`, which only matches regular queries. By default, `queryFilter()` uses `type: "any"` to match both regular and infinite queries. With `exact: true`, it generates the same key as `queryKey(input)` and matches only that regular query. Use `infiniteQueryFilter` for exact infinite-query matching. ### infiniteQueryKey() [#infinitequerykey] Returns the query key for infinite queries: ```ts eden.posts.get.infiniteQueryKey() eden.posts.get.infiniteQueryKey({ limit: 10 }) eden.posts.get.infiniteQueryKey({ limit: 10 }, { initialCursor: 'start' }) ``` The initial cursor is part of an exact infinite-query key. The helper defaults it to `null` for optional cursor routes, matching `infiniteQueryOptions()`. Required cursor routes must pass the same explicit non-null `initialCursor` to both helpers. ### infiniteQueryFilter() [#infinitequeryfilter] Returns a filter object for infinite queries: ```ts eden.posts.get.infiniteQueryFilter() eden.posts.get.infiniteQueryFilter({ category: 'tech' }) eden.posts.get.infiniteQueryFilter( { category: 'tech' }, { initialCursor: 'start' } ) ``` Infinite-query filters match all initial-cursor variants by default. Pass `initialCursor` in the filter options to target one variant. With the default hash, cursor values are compared as complete values rather than partial object or array prefixes, while route input keeps TanStack Query's normal partial matching. A custom `queryKeyHashFn` defines cursor identity just as it does for cache entries. On optional cursor routes, `exact: true` uses the default `null` cursor when `initialCursor` is omitted. Required cursor routes allow an omitted cursor only for broad filters with `exact` absent or `false`; exact and cursor-specific filters require an explicit non-null cursor. ### mutationKey() [#mutationkey] Returns the mutation key for a mutation route: ```ts eden.users.post.mutationKey() // => [['users', 'post']] ``` ## Cache Operations [#cache-operations] ### invalidateQueries [#invalidatequeries] Mark queries as stale and trigger a refetch: ```tsx const queryClient = useQueryClient() const eden = useEden() // Invalidate all users queries (partial matching) queryClient.invalidateQueries({ queryKey: eden.users.get.queryKey() }) // Invalidate with queryFilter queryClient.invalidateQueries( eden.users.get.queryFilter() ) // Invalidate a specific user queryClient.invalidateQueries({ queryKey: eden.users({ id: '1' }).get.queryKey() }) ``` ### cancelQueries [#cancelqueries] Cancel in-flight queries (useful for optimistic updates): ```tsx await queryClient.cancelQueries({ queryKey: eden.todos.get.queryKey() }) ``` ### getQueryData / setQueryData [#getquerydata--setquerydata] Read and write the cache directly: ```tsx // Read cached data const user = queryClient.getQueryData( eden.users({ id: '1' }).get.queryKey() ) // Write to cache queryClient.setQueryData( eden.users({ id: '1' }).get.queryKey(), { id: '1', name: 'Updated Name', email: 'updated@example.com' } ) ``` ### removeQueries [#removequeries] Remove queries from the cache entirely: ```tsx queryClient.removeQueries({ queryKey: eden.users({ id: '1' }).get.queryKey() }) ``` ## Partial Matching [#partial-matching] TanStack Query uses partial matching for query keys. A key without input matches all queries for that route: ```tsx // Invalidates ALL users queries, regardless of input queryClient.invalidateQueries({ queryKey: eden.users.get.queryKey() }) // Only invalidates the specific query with role='admin' queryClient.invalidateQueries({ queryKey: eden.users.get.queryKey({ role: 'admin' }) }) ``` This is useful for broad invalidation after mutations: ```tsx const createUser = useMutation({ ...eden.users.post.mutationOptions(), onSuccess: () => { // Invalidates all users queries (list, filtered, by id, etc.) queryClient.invalidateQueries({ queryKey: eden.users.get.queryKey() }) }, }) ``` For more details on how partial matching works, see the [TanStack Query docs on query invalidation](https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation). # Server-Side Rendering (/docs/guides/ssr) # Server-Side Rendering [#server-side-rendering] Eden TanStack Query supports SSR with `createEdenOptionsProxy`, which creates a server-side proxy for prefetching data that hydrates seamlessly on the client. ## createEdenOptionsProxy [#createedenoptionsproxy] `createEdenOptionsProxy` binds to an Eden client and generates the same query keys as the client-side `useEden()` hook. Pass its options to `QueryClient` methods to prefetch data: ```ts import { createEdenOptionsProxy } from 'eden-tanstack-react-query' const eden = createEdenOptionsProxy({ client: serverClient }) // Use the same API as useEden() await queryClient.prefetchQuery(eden.users.get.queryOptions()) ``` ## Astro [#astro] The [runnable Astro example](https://github.com/xkelxmc/eden-tanstack-query/tree/main/examples/astro) uses an Astro SSR page and one React `client:load` island. The page creates a request-local QueryClient, prefetches through `createEdenOptionsProxy`, and passes its dehydrated state as serializable island props. Keep QueryClientProvider, EdenProvider, HydrationBoundary, and the components that use them inside the same React island. React context does not cross Astro island boundaries. Create a fresh server QueryClient per request and keep the browser instance stable across renders. A shared stale time prevents an immediate refetch after hydration; mutations invalidate the same list query key. The example includes a same-origin Elysia API and a Bun start command for the standalone Astro adapter output. Its README covers development, builds, and the shared in-memory demo storage. ## Next.js App Router [#nextjs-app-router] The [runnable Next.js example](https://github.com/xkelxmc/eden-tanstack-query/tree/main/examples/nextjs) includes a request-local QueryClient, server prefetching, native HydrationBoundary, and a client mutation that invalidates the list. It runs one Bun Elysia API behind a same-origin Next.js rewrite, so server renders and browser requests share the same demo data. Its README covers the root development and production commands. ### Eden clients [#eden-clients] These examples import your Elysia app's `App` type from `@/lib/server`. Set `NEXT_PUBLIC_API_URL` to the API URL reachable by the browser and `INTERNAL_API_URL` to the same API's URL reachable by the Next.js server. Keep the React context in a client module: ```ts title="lib/eden.ts" 'use client' import { treaty } from '@elysiajs/eden' import { createEdenTanStackQuery } from 'eden-tanstack-react-query' import type { App } from '@/lib/server' export const { EdenProvider, useEden } = createEdenTanStackQuery() export const edenClient = treaty(process.env.NEXT_PUBLIC_API_URL!) ``` For cookie authentication with a cross-origin API, pass `{ fetch: { credentials: 'include' } }` as the second argument to `treaty`. Configure the API's CORS to allow credentials and the Next.js application's exact origin. Cross-origin requests can still be same-site, such as requests between subdomains. If the API is also cross-site, its cookies typically need `SameSite=None; Secure`, and browser restrictions on third-party cookies still apply. Same-origin requests and APIs without cookie authentication can keep the default configuration. Define the server factory separately. Install `server-only` with `bun add server-only` so Next.js rejects imports of this module from client code. ```ts title="lib/eden-server.ts" import 'server-only' import { treaty } from '@elysiajs/eden' import type { App } from '@/lib/server' export function getServerEdenClient() { return treaty(process.env.INTERNAL_API_URL!) } ``` This factory makes unauthenticated requests. If your API requires authentication, pass the current request's credentials through your server-side Treaty configuration. ### Layout and Providers [#layout-and-providers] ```tsx title="app/layout.tsx" import { Providers } from './providers' export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` ```tsx title="app/providers.tsx" 'use client' import { useState } from 'react' import { QueryClientProvider } from '@tanstack/react-query' import { getQueryClient } from '@/lib/query-client' import { EdenProvider, edenClient } from '@/lib/eden' export function Providers({ children }: { children: React.ReactNode }) { const [queryClient] = useState(() => getQueryClient()) return ( {children} ) } ``` ### QueryClient Factory [#queryclient-factory] Create a fresh QueryClient on the server for each request. In the browser, reuse one instance across renders. ```ts title="lib/query-client.ts" import { QueryClient } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, } } }) } let browserQueryClient: QueryClient | undefined export function getQueryClient() { if (typeof window === 'undefined') { return makeQueryClient() } if (!browserQueryClient) { browserQueryClient = makeQueryClient() } return browserQueryClient } ``` ### Server Component Prefetching [#server-component-prefetching] ```tsx title="app/users/page.tsx" import { HydrationBoundary, dehydrate } from '@tanstack/react-query' import { createEdenOptionsProxy } from 'eden-tanstack-react-query' import { getQueryClient } from '@/lib/query-client' import { getServerEdenClient } from '@/lib/eden-server' import type { App } from '@/lib/server' import { UserList } from './user-list' export default async function UsersPage() { const queryClient = getQueryClient() const serverClient = getServerEdenClient() const eden = createEdenOptionsProxy({ client: serverClient }) // Prefetch on server await queryClient.prefetchQuery(eden.users.get.queryOptions()) return ( ) } ``` ```tsx title="app/users/user-list.tsx" 'use client' import { useQuery } from '@tanstack/react-query' import { useEden } from '@/lib/eden' export function UserList() { const eden = useEden() // Uses prefetched data -- no loading state on initial render const { data: users } = useQuery(eden.users.get.queryOptions()) return (
    {users?.map(user => (
  • {user.name}
  • ))}
) } ``` ### Prefetching Multiple Queries [#prefetching-multiple-queries] ```tsx export default async function DashboardPage() { const queryClient = getQueryClient() const eden = createEdenOptionsProxy({ client: getServerEdenClient() }) await Promise.all([ queryClient.prefetchQuery(eden.users.get.queryOptions()), queryClient.prefetchQuery(eden.stats.get.queryOptions()), ]) return ( ) } ``` ## Next.js Pages Router [#nextjs-pages-router] The same pattern works with `getServerSideProps` when the entire dehydrated state is JSON-compatible, as required by the [Next.js Pages props contract](https://nextjs.org/docs/pages/api-reference/functions/get-server-side-props#props): ```tsx title="pages/users/index.tsx" import type { GetServerSideProps } from 'next' import { dehydrate, QueryClient } from '@tanstack/react-query' import { createEdenOptionsProxy } from 'eden-tanstack-react-query' import { treaty } from '@elysiajs/eden' import type { App } from '@/lib/server' export const getServerSideProps: GetServerSideProps = async () => { const queryClient = new QueryClient() const serverClient = treaty(process.env.INTERNAL_API_URL!) const eden = createEdenOptionsProxy({ client: serverClient }) await queryClient.prefetchQuery(eden.users.get.queryOptions()) return { props: { dehydratedState: dehydrate(queryClient) } } } ``` Treaty [parses date strings into `Date` objects by default](https://elysiajs.com/eden/treaty/config#parsedate). `dehydrate` retains those objects, and Pages Router rejects them in raw props. If your cache contains dates or other non-JSON values, serialize the dehydrated state with a serializer that supports those types before returning it in props. Apply its matching deserializer in `_app.tsx` before passing the state to `HydrationBoundary`, during both server rendering and browser hydration. This whole-state serialization also preserves dates in `queryKey`. Eden retains `Date` objects in query-key inputs, so use a Date-aware whole-state serializer when those inputs contain dates. If only query data needs conversion and the rest of the dehydrated state is JSON-compatible, you can instead pair `dehydrate(queryClient, { serializeData })` with `HydrationBoundary`'s `options={{ defaultOptions: { deserializeData } }}`. Alternatively, configure `defaultOptions.dehydrate.serializeData` and `defaultOptions.hydrate.deserializeData` on your QueryClients, as shown in the [TanStack Query advanced SSR guide](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#streaming-with-server-components). These hooks transform query data only, not query keys. Configure deserialization for both server rendering and browser hydration. Whole-state serialization already handles query data, so it does not require these hooks as well. A plain `JSON.parse(JSON.stringify(...))` round trip turns dates into strings and loses the runtime types expected by query consumers. Disabling date parsing only on the server also makes hydrated data differ from browser refetches. Keep the response parsing consistent across server and browser clients. Wrap your app with `HydrationBoundary` in `_app.tsx` and provide `EdenProvider` as shown in the [Getting Started](/docs/getting-started) guide. See the [TanStack Query SSR docs](https://tanstack.com/query/latest/docs/framework/react/guides/ssr) for the full Pages Router setup and serialization options. ## TanStack Start [#tanstack-start] The [authenticated Start example](https://github.com/xkelxmc/eden-tanstack-query/tree/main/examples/authenticated-start) includes a same-origin Elysia API, request-local cookie forwarding, native Router/Query hydration, and session switching. Its README has development and single-server build commands. Use `ensureQueryData` in route loaders for server-side prefetching: ```tsx title="app/routes/users.tsx" import { createFileRoute } from '@tanstack/react-router' import { createEdenOptionsProxy } from 'eden-tanstack-react-query' import { treaty } from '@elysiajs/eden' import type { App } from '@/lib/server' export const Route = createFileRoute('/users')({ loader: async ({ context: { queryClient } }) => { const serverClient = treaty(import.meta.env.VITE_API_URL) const eden = createEdenOptionsProxy({ client: serverClient }) await queryClient.ensureQueryData(eden.users.get.queryOptions()) }, component: UsersPage }) ``` See the [TanStack Start docs](https://tanstack.com/start/latest/docs/framework/react/guide/ssr) for router and QueryClient setup. # Testing (/docs/guides/testing) # Testing [#testing] Testing patterns for components that use eden-tanstack-query, using Vitest and Testing Library. ## Setup [#setup] ### Dependencies [#dependencies] ```bash bun add -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom @vitejs/plugin-react ``` ### Vitest Config [#vitest-config] The examples assume Vitest resolves `@/` to your application's `src` directory. Configure that alias or adjust the imports to match your project. ```ts title="vitest.config.ts" import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', setupFiles: ['./test/setup.ts'], globals: true } }) ``` ### Setup File [#setup-file] ```ts title="test/setup.ts" import '@testing-library/jest-dom/vitest' import { afterEach, vi } from 'vitest' import { cleanup } from '@testing-library/react' afterEach(() => { cleanup() vi.restoreAllMocks() }) ``` ## Test Utilities [#test-utilities] ### Test QueryClient [#test-queryclient] ```ts title="test/query-client.ts" import { QueryClient } from '@tanstack/react-query' export function createTestQueryClient() { return new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 }, mutations: { retry: false } } }) } ``` ### Test Wrapper [#test-wrapper] Import `EdenProvider` from the same application module as `useEden`. Each call to `createEdenTanStackQuery` creates separate React contexts, so a new factory in the test wrapper cannot provide the context used by the application's hook. ```tsx title="test/wrapper.tsx" import type { ReactNode } from 'react' import { QueryClientProvider } from '@tanstack/react-query' import { EdenProvider } from '@/lib/eden' import { createTestQueryClient } from './query-client' export function createTestWrapper(mockClient: any) { const queryClient = createTestQueryClient() function Wrapper({ children }: { children: ReactNode }) { return ( {children} ) } return { Wrapper, queryClient } } ``` ### Mock Eden Client [#mock-eden-client] This mock implements a subset of the application's routes. The wrapper's `any` parameter allows this partial test double; it does not verify that the mock matches the full Eden client type. Adjust the mock to match the routes and response data your components use. ```ts title="test/mocks/eden-client.ts" import { vi } from 'vitest' export function createMockEdenClient() { return { users: Object.assign( (params: { id: string }) => ({ get: vi.fn().mockResolvedValue({ data: { id: params.id, name: 'User', email: 'user@example.com' }, error: null }), }), { get: vi.fn().mockResolvedValue({ data: [ { id: '1', name: 'Alice', email: 'alice@example.com' }, { id: '2', name: 'Bob', email: 'bob@example.com' } ], error: null }), post: vi.fn().mockResolvedValue({ data: { id: '3', name: 'New User', email: 'new@example.com' }, error: null }), } ) } } ``` ## Testing a Query [#testing-a-query] ```tsx title="test/components/user-list.test.tsx" import { describe, it, expect } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' import { useQuery } from '@tanstack/react-query' import { useEden } from '@/lib/eden' import { createTestWrapper } from '../wrapper' import { createMockEdenClient } from '../mocks/eden-client' function UserList() { const eden = useEden() const { data: users, isPending } = useQuery(eden.users.get.queryOptions()) if (isPending) return
Loading...
return (
    {users?.map(user =>
  • {user.name}
  • )}
) } describe('UserList', () => { it('renders users after loading', async () => { const mockClient = createMockEdenClient() const { Wrapper } = createTestWrapper(mockClient) render(, { wrapper: Wrapper }) expect(screen.getByText('Loading...')).toBeInTheDocument() await waitFor(() => { expect(screen.getByText('Alice')).toBeInTheDocument() expect(screen.getByText('Bob')).toBeInTheDocument() }) }) }) ``` ## Testing a Mutation [#testing-a-mutation] ```tsx title="test/components/create-user.test.tsx" import { describe, it, expect } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useMutation } from '@tanstack/react-query' import { useEden } from '@/lib/eden' import { createTestWrapper } from '../wrapper' import { createMockEdenClient } from '../mocks/eden-client' function CreateUserForm() { const eden = useEden() const createUser = useMutation(eden.users.post.mutationOptions()) return (
{ e.preventDefault() const formData = new FormData(e.currentTarget) createUser.mutate({ name: formData.get('name') as string, email: formData.get('email') as string, }) }}> {createUser.isSuccess && Created!}
) } describe('CreateUserForm', () => { it('submits form and calls mutation', async () => { const user = userEvent.setup() const mockClient = createMockEdenClient() const { Wrapper } = createTestWrapper(mockClient) render(, { wrapper: Wrapper }) await user.type(screen.getByPlaceholderText('Name'), 'New User') await user.type(screen.getByPlaceholderText('Email'), 'new@example.com') await user.click(screen.getByText('Create')) await waitFor(() => { expect(screen.getByText('Created!')).toBeInTheDocument() }) expect(mockClient.users.post).toHaveBeenCalledWith({ name: 'New User', email: 'new@example.com' }) }) }) ``` # Introduction (/docs) # Eden TanStack Query [#eden-tanstack-query] Type-safe TanStack Query integration for Elysia Eden. Like `@trpc/react-query`, but for Elysia. ## Features [#features] - **End-to-end type safety** - Full TypeScript inference from Elysia routes - **Native TanStack Query patterns** - Use standard `useQuery`, `useMutation`, `useInfiniteQuery` - **Query options factories** - `queryOptions()`, `mutationOptions()`, `infiniteQueryOptions()` - **Automatic query key generation** - Type-safe keys derived from route paths - **Path parameter support** - `eden.users({ id: '1' }).get.queryOptions()` - **Query invalidation helpers** - `queryFilter()` for cache management ## Installation [#installation] Requires `@tanstack/react-query` 5.102.8 or newer within v5. The peer range is `^5.102.8`. ```npm npm i eden-tanstack-react-query @tanstack/react-query @elysiajs/eden elysia ``` ## Quick Start [#quick-start] ### 1. Define your Elysia server [#1-define-your-elysia-server] ```ts title="server.ts" import { Elysia, t } from 'elysia' const posts = [ { id: 1, title: 'First post' }, { id: 2, title: 'Second post' }, { id: 3, title: 'Third post' } ] const app = new Elysia() .get('/users', () => [ { id: '1', name: 'Alice' }, { id: '2', name: 'Bob' } ]) .get('/users/:id', ({ params }) => ({ id: params.id, name: `User ${params.id}` })) .post('/users', ({ body }) => ({ id: String(Date.now()), ...body }), { body: t.Object({ name: t.String() }) }) .get('/posts', ({ query }) => { const cursor = query.cursor ?? 0 const limit = query.limit ?? 10 const items = posts.slice(cursor, cursor + limit) const nextCursor = cursor + items.length < posts.length ? cursor + items.length : null return { items, nextCursor } }, { query: t.Object({ cursor: t.Optional(t.Numeric({ minimum: 0, multipleOf: 1 })), limit: t.Optional(t.Numeric({ minimum: 1, multipleOf: 1 })) }) }) .listen(3000) export type App = typeof app ``` ### 2. Create typed hooks [#2-create-typed-hooks] ```ts title="lib/eden.ts" import { createEdenTanStackQuery } from 'eden-tanstack-react-query' import { treaty } from '@elysiajs/eden' import type { App } from '../server' export const { EdenProvider, useEden, useEdenClient } = createEdenTanStackQuery() export const edenClient = treaty('http://localhost:3000') ``` ### 3. Set up providers [#3-set-up-providers] ```tsx title="App.tsx" import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { EdenProvider, edenClient } from './lib/eden' const queryClient = new QueryClient() function App() { return ( ) } ``` ### 4. Use in components [#4-use-in-components] ```tsx title="UserList.tsx" import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useEden } from './lib/eden' function UserList() { const eden = useEden() const queryClient = useQueryClient() // Query const { data: users } = useQuery(eden.users.get.queryOptions()) // Query with path params const { data: user } = useQuery( eden.users({ id: '1' }).get.queryOptions() ) // Mutation with cache invalidation const createUser = useMutation({ ...eden.users.post.mutationOptions(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: eden.users.get.queryKey() }) } }) return (
    {users?.map(user =>
  • {user.name}
  • )}
) } ``` ## API Reference [#api-reference] ### createEdenTanStackQuery [#createedentanstackquery] Creates typed providers and hooks for your Elysia app. ```ts const { EdenProvider, useEden, useEdenClient } = createEdenTanStackQuery() ``` ### useEden [#useeden] Returns the Eden options proxy with methods for each route. #### Query Methods (GET, HEAD, OPTIONS) [#query-methods-get-head-options] | Method | Description | |--------|-------------| | `.queryOptions(input, opts?)` | Options for `useQuery`; input is optional only when the route query is omittable | | `.queryKey(input?)` | Query key for cache operations | | `.infiniteQueryOptions(input, opts)` | Options for `useInfiniteQuery`; available for supported non-union cursor routes | #### Mutation Methods (POST, PUT, PATCH, DELETE) [#mutation-methods-post-put-patch-delete] | Method | Description | |--------|-------------| | `.mutationOptions(opts?)` | Options for `useMutation` | | `.mutationKey()` | Mutation key | ### Path Parameters [#path-parameters] Access routes with path params using function calls: ```ts // Route: /users/:id eden.users({ id: '123' }).get.queryOptions() // Nested: /posts/:postId/comments/:commentId eden.posts({ postId: '1' }).comments({ commentId: '2' }).get.queryOptions() ``` ### Query with Input [#query-with-input] Pass query parameters or request body: ```ts // Query params: GET /users?role=admin eden.users.get.queryOptions({ role: 'admin' }) // With TanStack Query options eden.users.get.queryOptions( { role: 'admin' }, { staleTime: 5 * 60 * 1000 } ) ``` ### Infinite Queries [#infinite-queries] For paginated data. Optional cursor fields default to `null`; required cursor fields need an explicit non-null `initialCursor`. Union query schemas do not expose infinite-query methods because input and cursor variants cannot be correlated safely. ```tsx import { useInfiniteQuery } from '@tanstack/react-query' const { data, fetchNextPage, hasNextPage } = useInfiniteQuery( eden.posts.get.infiniteQueryOptions( { limit: 10 }, { getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, initialCursor: null, } ) ) ``` ## Comparison [#comparison] | Feature | eden-tanstack-react-query | @ap0nia/eden-react-query | |---------|---------------------------|--------------------------| | API Style | `useQuery(eden.users.get.queryOptions())` | `eden.users.get.useQuery()` + `useUtils()` | | TanStack Query Native | ✅ Standard hooks only | ⚠️ Wrapper hooks | | Query Keys | Explicit `queryKey()` / `queryFilter()` | tRPC-style utils | | Learning Curve | Standard TanStack Query | tRPC-style API | # LLMs (/docs/llms) # LLMs [#llms] This documentation is optimized for AI assistants and LLM-powered tools. ## Available Endpoints [#available-endpoints] | URL | Description | |-----|-------------| | [/llms-full.txt](/llms-full.txt) | Complete documentation in a single file | | `/docs.mdx` | Any docs page as Markdown (append `.mdx` to URL) | ## Usage with AI Assistants [#usage-with-ai-assistants] Copy and paste these URLs when chatting with AI: ``` https://eden-tanstack-query.vercel.app/llms-full.txt ``` Or use the **Copy Markdown** button on any documentation page to copy the content directly. ## Examples [#examples] Ask your AI assistant: > Read https://eden-tanstack-query.vercel.app/llms-full.txt and explain how to use Eden TanStack Query with mutations. > Read https://eden-tanstack-query.vercel.app/docs.mdx and show me a quick start example. # Troubleshooting (/docs/troubleshooting) # Troubleshooting [#troubleshooting] ## Installation [#installation] ### Peer Dependencies [#peer-dependencies] Ensure all required packages are installed: ```bash bun add eden-tanstack-react-query @tanstack/react-query @elysiajs/eden elysia ``` ### Version Compatibility [#version-compatibility] | Package | Minimum Version | |---------|-----------------| | `@tanstack/react-query` | 5.102.8, within v5 | | `@elysiajs/eden` | 1.3.0 | | `elysia` | 1.3.0 | | `typescript` | 5.5.2 | React Query versions below 5.102.8 are outside the supported peer range. See [Upgrading from an earlier release](/docs/getting-started#upgrading-from-an-earlier-release) for the dependency migration. --- ## Provider Errors [#provider-errors] ### "useEden must be used within an EdenProvider" [#useeden-must-be-used-within-an-edenprovider] The `useEden` hook is called outside the provider tree. ```tsx // Wrong — useEden is outside provider function App() { const eden = useEden() // Error! return ( ) } // Correct — useEden inside provider function App() { return ( {/* useEden works here */} ) } ``` ### "No QueryClient set" [#no-queryclient-set] Ensure `QueryClientProvider` wraps `EdenProvider`: ```tsx function App() { return ( ) } ``` --- ## Cache Issues [#cache-issues] ### Cache Not Updating After Mutation [#cache-not-updating-after-mutation] Mutations don't automatically refresh queries. Use `invalidateQueries` in `onSuccess`: ```tsx const createUser = useMutation({ ...eden.users.post.mutationOptions(), onSuccess: () => { queryClient.invalidateQueries(eden.users.get.queryFilter()) } }) ``` ### Wrong Data in Cache for Path Params [#wrong-data-in-cache-for-path-params] If different path params return the same data, verify query keys differ: ```tsx console.log(eden.users({ id: '1' }).get.queryKey()) // [['users', 'get'], { // pathParams: [{ pathIndex: 0, entries: [['id', '1']] }], // type: 'query' // }] console.log(eden.users({ id: '2' }).get.queryKey()) // [['users', 'get'], { // pathParams: [{ pathIndex: 0, entries: [['id', '2']] }], // type: 'query' // }] ``` If keys are identical, update to the latest version of `eden-tanstack-react-query`. --- ## Type Errors [#type-errors] ### "Property does not exist on type" [#property-does-not-exist-on-type] The proxy mirrors your Elysia routes. If a property doesn't exist: 1. Verify the route exists on your Elysia app 2. Ensure you export the type: `export type App = typeof app` (not just `export { app }`) 3. Restart TS server: `Cmd/Ctrl + Shift + P` > "TypeScript: Restart TS Server" ### Generic Inference Shows `unknown` [#generic-inference-shows-unknown] Don't reassign the Elysia app variable — it loses type inference: ```ts // Bad — loses types let app = new Elysia() app = app.get('/users', () => [...]) // Good — chain methods preserves types const app = new Elysia() .get('/users', () => [...]) ``` Also ensure you use `import type`: ```ts import type { App } from '../server' ``` ### Numeric Path Parameters [#numeric-path-parameters] Path params accept both strings and numbers (`string | number`). Values are serialized into the URL, and the library normalizes them when building the query key, so both forms address the same cache entry: ```ts // Both compile and hit the same cache entry eden.users({ id: 123 }).get.queryOptions() eden.users({ id: '123' }).get.queryOptions() ``` ### Query Parameters Not Typed [#query-parameters-not-typed] Add a query schema to your Elysia route for types to flow through: ```ts app.get('/users', ({ query }) => { // ... }, { query: t.Object({ search: t.Optional(t.String()), limit: t.Optional(t.Number()) }) }) ``` --- ## Monorepo Issues [#monorepo-issues] ### Types Not Syncing Across Packages [#types-not-syncing-across-packages] Export the app type from your server package: ```ts title="packages/server/src/index.ts" export type { App } from './app' ``` ```ts title="packages/client/src/eden.ts" import type { App } from '@myorg/server' ``` Use TypeScript project references if needed: ```json title="packages/client/tsconfig.json" { "references": [{ "path": "../server" }] } ``` --- ## Getting Help [#getting-help] If your issue isn't covered here: 1. **Search [GitHub Issues](https://github.com/xkelxmc/eden-tanstack-query/issues)** — your problem might already be reported 2. **Open an issue** with package versions, minimal reproduction, and error messages 3. Use [React Query DevTools](https://tanstack.com/query/latest/docs/framework/react/devtools) for debugging cache issues