Queries
Fetch data with type-safe query options
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
Using the server from the Getting Started guide:
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 <div>Loading...</div>
if (error) return <div>Request failed with status {error.status}</div>
return (
<ul>
{data?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}Query with Input
Pass query parameters as the first argument:
// 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:
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:
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:
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:
const { data } = useQuery(
eden.users.get.queryOptions(
{ role: 'admin' },
{ staleTime: 5 * 60 * 1000 }
)
)Path Parameters
For routes with dynamic segments, call the path segment as a function. See the Path Parameters guide for details.
// GET /users/:id
const { data } = useQuery(
eden.users({ id: userId }).get.queryOptions()
)useSuspenseQuery
Works with useSuspenseQuery for React Suspense support:
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 <div>{data.name}</div>
}Query Key
Use queryKey() for cache operations:
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 guide for full details on key structure and cache operations.
Query Filter
Use queryFilter() for invalidation with additional filter options:
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
Use skipToken to conditionally disable queries:
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 <div>Select a user</div>
return <div>{data?.name}</div>
}Abort on Unmount
Enable request cancellation when the component unmounts:
const { data } = useQuery(
eden.users.get.queryOptions(
undefined,
{
eden: {
abortOnUnmount: true
}
}
)
)Prefetching
Prefetch data before it's needed:
const queryClient = useQueryClient()
const eden = useEden()
const handleMouseEnter = () => {
queryClient.prefetchQuery(
eden.users({ id: userId }).get.queryOptions()
)
}Error Handling
Errors are typed as EdenFetchError: status is the HTTP status code and
value is the response body.
const { data, error } = useQuery(eden.users.get.queryOptions())
if (error) {
if (error.status === 401) {
return <div>Please log in</div>
}
return <div>Request failed with status {error.status}</div>
}error.value is typed as unknown unless the route declares its error
responses. Declare them in Elysia to get typed narrowing by status:
// server
.get('/items/:id', handler, {
response: {
200: t.Object({ name: t.String() }),
404: t.Object({ message: t.String() }),
},
})// client
if (error) {
if (error.status === 404) {
// error.value is narrowed to { message: string }
return <div>{error.value.message}</div>
}
return <div>Request failed with status {error.status}</div>
}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.