npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

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

About

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

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

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

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

Open Software & Tools

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

© 2026 – Pkg Stats / Ryan Hefner

eden-tanstack-query

v0.0.9

Published

TanStack Query integration for Eden Treaty

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-core

Basic 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 response

Mutations

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 typed

Query 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 - The EdenFetchError with status and value
  • queryKey - The generated query key
  • method - HTTP method ('get', 'post', etc.)
  • path - API path segments (['users', 'posts'])
  • input - The request input
  • type - 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[] } | undefined

When 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 } | undefined

When 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:

  • NarrowedData type correctly excludes error shapes
  • queryFn return 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 config
  • query.data may show as unknown or 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 typed

Advanced 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