eden-tanstack-query
v0.0.9
Published
TanStack Query integration for Eden Treaty
Maintainers
Readme
eden-tanstack-query
A TanStack Query integration for Eden Treaty, the type-safe client for Elysia.
Features
- 🔗 Type-safe - Full TypeScript inference from Elysia server types
- ⚡ Framework-agnostic - Works with React Query, Svelte Query, Solid Query, Vue Query
- 🎯 queryOptions - Reusable, type-safe query configurations
- 🔄 mutationOptions - Type-safe mutation configurations
- 🔑 Query keys - Auto-generated, type-safe query keys for cache operations
- ⚠️ Error handling - Configurable error throwing
- 🛠️ Eden integration - Seamlessly integrates with existing Treaty clients
Installation
bun add eden-tanstack-query @tanstack/query-coreBasic Usage
Setup
import { treaty } from '@elysiajs/eden/treaty2'
import { createEdenQuery } from 'eden-tanstack-query'
import { useQuery } from '@tanstack/svelte-query'
// Your Elysia app type
type App = {
users: {
get: {
query: { page?: number }
response: { users: User[] }
}
post: {
body: { name: string, email: string }
response: { user: User }
}
}
}
// Create Eden Query client
const eden = createEdenQuery<App>('http://localhost:8080')Queries
// Basic query (auto-generated query key)
const query = useQuery(eden.users.get.queryOptions())
// Query with parameters
const query = useQuery(
eden.users.get.queryOptions({ query: { page: 1 } })
)
// Access the data
query.data?.users // Fully typed from your Elysia responseMutations
import { useMutation } from '@tanstack/svelte-query'
// Basic mutation
const mutation = useMutation(eden.users.post.mutationOptions())
// Using the mutation
mutation.mutate({ name: 'John', email: '[email protected]' })
// Access the response
mutation.data?.user // Fully typedQuery Keys
// Get type-safe query keys for cache operations
const usersKey = eden.users.get.queryKey()
// Invalidate queries
const queryClient = useQueryClient()
queryClient.invalidateQueries({ queryKey: usersKey })
// Get query data type-safely
const data = queryClient.getQueryData(eden.users.get.queryKey())Error Handling
Global Error Handler
Define error handling logic once when creating the client:
import type { EdenErrorContext } from 'eden-tanstack-query'
const eden = createEdenQuery<App>('http://localhost:8080', {
throwOnError: true,
onError: ({ error, path, method, type }: EdenErrorContext) => {
// Runs for ALL queries and mutations before throwing
if (error.status === 401) {
authStore.logout()
router.push('/login')
}
if (error.status === 403) {
toast.error('Not authorized')
}
if (error.status >= 500) {
toast.error('Server error, please try again')
logger.error('API Error', { path, method, error })
}
}
})The EdenErrorContext provides:
error- TheEdenFetchErrorwith status and valuequeryKey- The generated query keymethod- HTTP method ('get', 'post', etc.)path- API path segments (['users', 'posts'])input- The request inputtype- Either 'query' or 'mutation'
Throw on Error (Default)
const eden = createEdenQuery<App>('http://localhost:8080', {
throwOnError: true
})
// Per-query error handling (in addition to global handler)
useQuery(eden.users.get.queryOptions(undefined, {
onError: (error: EdenFetchError) => {
// Runs after global handler, only for this query
if (error.status === 404) {
// Handle not found specifically for this query
}
}
}))Conditional Throwing
const eden = createEdenQuery<App>('http://localhost:8080', {
throwOnError: (queryKey, status) => {
// Don't throw on 404 (not found)
if (status === 404) return false
// Throw on server errors
if (status >= 500) return true
return false
}
})Type-Safe Data Narrowing
When throwOnError is true (the default), errors are thrown before reaching callbacks like select and onSuccess. The library automatically narrows the data type to exclude error shapes:
// Given an endpoint that returns:
// - Success: { users: User[] }
// - Error: { error: string }
// Default: throwOnError = true (or omitted)
const eden = createEdenQuery<App>('http://localhost:8080')
// OR explicitly:
const eden = createEdenQuery<App>('http://localhost:8080', { throwOnError: true })
useQuery(eden.users.get.queryOptions(undefined, {
select: (data) => {
// data: { users: User[] }
// Error shape is excluded - errors throw before reaching here
return data.users // No type guard needed
},
onSuccess: (data) => {
// data: { users: User[] }
console.log(data.users)
}
}))
// query.data type: { users: User[] } | undefinedWhen throwOnError is false, the full union type is preserved since errors don't throw:
// Explicit: throwOnError = false
const eden = createEdenQuery<App>('http://localhost:8080', {
throwOnError: false
})
useQuery(eden.users.get.queryOptions(undefined, {
select: (data) => {
// data: { users: User[] } | { error: string }
// Must handle both cases since errors don't throw
if ('error' in data) return null
return data.users
},
onSuccess: (data) => {
// data: { users: User[] } | { error: string }
if ('error' in data) {
console.log('Error:', data.error)
} else {
console.log(data.users)
}
}
}))
// query.data type: { users: User[] } | { error: string } | undefinedWhen throwOnError is a function, the full union type is also preserved (since throwing is conditional):
// Conditional throwing
const eden = createEdenQuery<App>('http://localhost:8080', {
throwOnError: (queryKey, status) => status >= 500
})
useQuery(eden.users.get.queryOptions(undefined, {
select: (data) => {
// data: { users: User[] } | { error: string }
// Full union - some errors may not throw (e.g., 404)
if ('error' in data) return null
return data.users
}
}))Known Limitation: createQuery Inference
The type narrowing works correctly at the library level (queryFn return type), but TanStack Query's createQuery/useQuery may not always infer the narrowed type due to complex generic inference.
What works:
NarrowedDatatype correctly excludes error shapesqueryFnreturn type is narrowed at the library level- Query keys are correctly typed
- Direct access to options properties
What may vary:
createQuery(options)inference depends on framework version and TS configquery.datamay show asunknownor full union in some cases
Workarounds:
// 1. Use select to transform with explicit types
useQuery(eden.users.get.queryOptions(undefined, {
select: (data) => data.users // data is narrowed here
}))
// 2. Add explicit type annotation
const query = useQuery(eden.users.get.queryOptions()) as CreateQueryResult<{ users: User[] }>
// 3. Access queryFn directly for fully typed results
const options = eden.users.get.queryOptions()
const data = await options.queryFn() // Correctly typedAdvanced Usage
Custom Eden Treaty Options
const eden = createEdenQuery<App>('http://localhost:8080', {
treaty: {
headers: { authorization: 'Bearer token' },
fetch: customFetch
}
})
useQuery(eden.users.get.queryOptions(
{ query: { page: 1 } },
{
eden: {
headers: { 'X-Custom': 'value' }
},
staleTime: 5000
}
))Query Key Prefix
const eden = createEdenQuery<App>('http://localhost:8080', {
queryKeyPrefix: 'my-api'
})
// Keys will be prefixed: ['my-api', 'users', 'get']Using with React Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
const query = useQuery(eden.users.get.queryOptions())
const mutation = useMutation(eden.users.post.mutationOptions())
const queryClient = useQueryClient()Using with Solid Query
import { createQuery, createMutation } from '@tanstack/solid-query'
const query = createQuery(() => eden.users.get.queryOptions())
const mutation = createMutation(() => eden.users.post.mutationOptions())Using with Vue Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
const query = useQuery(eden.users.get.queryOptions())
const mutation = useMutation(eden.users.post.mutationOptions())
const queryClient = useQueryClient()Query Key Structure
Query keys are auto-generated from your API paths:
// Simple path
eden.users.get.queryKey()
// → ['users', 'get']
// Path with parameters
eden.users({ id: '123' }).get.queryKey()
// → ['users', { id: '123' }, 'get']
// Nested paths
eden.users.posts({ userId: '123' }).get.queryKey()
// → ['users', 'posts', { userId: '123' }, 'get']Type Safety
All types are fully inferred from your Elysia server:
- ✅ Query data type (from success responses)
- ✅ Error type (from EdenFetchError or Treaty response)
- ✅ Input validation (query params, body)
- ✅ Query keys (type-safe, auto-generated)
- ✅ Framework-agnostic (works with all TanStack Query variants)
License
MIT
