TanStack Query integration for Elysia Eden - type-safe queries and mutations with zero boilerplate.
Highlights:
- Auto-generated
queryKey,queryOptions,mutationOptions,mutation, and cache helpers - Type-safe data and error inference from your Elysia routes
- Works with any TanStack Query adapter (React, Svelte, Vue, Solid)
bun add eden-tanstack-query @elysiajs/eden @tanstack/query-core elysia
# or
npm install eden-tanstack-query @elysiajs/eden @tanstack/query-core elysiaimport { createEdenTQ } from "eden-tanstack-query";
import type { App } from "./server"; // Your Elysia app type
const eden = createEdenTQ<App>("http://localhost:3000");For large codebases, you can avoid pulling full app types into every client file:
import { createEdenTQFromSchema } from "eden-tanstack-query";
import type { App } from "./server";
type Routes = App["~Routes"];
const eden = createEdenTQFromSchema<Routes>("http://localhost:3000");This keeps the client typed while reducing type-checker pressure compared with importing a full Elysia app type everywhere.
import { createQuery } from "@tanstack/svelte-query"; // or react-query, vue-query, etc.
// Fully type-safe, auto-generated query key
const query = createQuery(() =>
eden.users({ id: "123" }).get.queryOptions({
params: { id: "123" },
}),
);
// query.data is typed as your Elysia response type!React example:
import { useQuery } from "@tanstack/react-query";
const query = useQuery(
eden.users({ id: "123" }).get.queryOptions({
params: { id: "123" },
}),
);import { createInfiniteQuery } from "@tanstack/svelte-query";
const infiniteQuery = createInfiniteQuery(() =>
eden.posts.get.infiniteQueryOptions(
{ query: { limit: "10" } },
{
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
// cursorKey: 'cursor' // optional, defaults to 'cursor'
},
),
);import { createMutation } from "@tanstack/svelte-query";
const mutation = createMutation(
eden.users.post.mutation({
onSuccess: (data) => {
console.log("Created user:", data.id);
},
}),
);
// Type-safe variables
mutation.mutate({
body: { name: "Alice", email: "alice@example.com" },
});When using @tanstack/svelte-query or @tanstack/solid-query, TypeScript can
sometimes widen mutation TData to undefined if mutationOptions(...) is
fully inlined inside createMutation(() => ...) / useMutation(() => ...).
Use one of these stable patterns:
import { createQuery, createMutation } from "@tanstack/svelte-query";
// Query: hoist options first
const userQueryOptions = eden.users({ id: "123" }).get.queryOptions({
params: { id: "123" },
});
const userQuery = createQuery(() => userQueryOptions);
// Mutation: prefer the built-in accessor helper
const createUserMutation = createMutation(
eden.users.post.mutation({
onSuccess: (data) => {
console.log(data.id);
},
}),
);Solid example:
import { useMutation } from "@tanstack/solid-query";
const createUserMutation = useMutation(
eden.users.post.mutation({
onSuccess: (data) => {
console.log(data.id);
},
}),
);If you need fully inline calls, you can also pin the generic:
type CreateUserResponse = App["~Routes"]["users"]["post"]["response"][200];
const mutation = createMutation(() =>
eden.users.post.mutationOptions<CreateUserResponse>({
onSuccess: (data) => {
console.log(data.id);
},
}),
);For routes like /cases/:id/workflow, you can now choose either pattern:
Inline param (known when building options):
const query = eden.cases({ id: "case-123" }).workflow.get.queryOptions({
params: { id: "case-123" },
});
const mutation = eden.cases({ id: "case-123" }).workflow.patch.mutationOptions();
await mutation.mutationFn({
params: { id: "case-123" },
body: { status: "active" },
});Deferred param (ID known later at call time):
const query = eden.cases({ id: "" }).workflow.get.queryOptions({
params: { id: caseId },
});
const mutation = eden.cases({ id: "" }).workflow.patch.mutationOptions();
await mutation.mutationFn({
params: { id: caseId },
body: { status: "active" },
});Recommendation:
- Use inline params when the route ID is already available.
- Use deferred params when creating reusable query/mutation configs before the ID is known.
import { useQueryClient } from "@tanstack/svelte-query";
const queryClient = useQueryClient();
// Invalidate specific query
await eden.users({ id: "123" }).get.invalidate(queryClient, {
params: { id: "123" },
});
// Invalidate all queries for a route
await eden.users({ id: "123" }).get.invalidate(queryClient);For tRPC-like ergonomics, use createEdenTQUtils to bind a QueryClient once:
import { createEdenTQ, createEdenTQUtils } from "eden-tanstack-query";
const eden = createEdenTQ<App>("http://localhost:3000");
const utils = createEdenTQUtils(eden, queryClient);
// No need to pass queryClient every time!
await utils.users({ id: "123" }).get.invalidate({ params: { id: "123" } });
await utils.posts.get.prefetch({ query: { limit: "10" } });
await utils.posts.get.cancel();
await utils.posts.get.refetch();
// Cache manipulation
utils.users({ id: "123" }).get.setData({ params: { id: "123" } }, { id: "123", name: "Updated" });
const cached = utils.users({ id: "123" }).get.getData({ params: { id: "123" } });queryFn and mutationFn throw when the Eden response has error, so TanStack
Query error states are populated automatically:
const options = eden.users({ id: "123" }).get.queryOptions({
params: { id: "123" },
});
try {
const data = await options.queryFn();
} catch (error) {
// error is typed from your Elysia response map
}If a route has no typed response schema (for example response: never),
queryFn/mutationFn data falls back to unknown instead of any.
Creates a type-safe Eden client with TanStack Query helpers.
domain: Your API URL or Elysia app instanceconfig.queryKeyPrefix: Custom prefix for query keys (default:['eden'])
Creates the same client from a route schema (App['~Routes']) instead of the full app type.
Use this when your editor/tsserver slows down with very large app types.
Creates a utils object with a bound QueryClient for tRPC-like ergonomics.
Each HTTP method (get, post, put, delete, patch) has:
| Method | Description |
|---|---|
.queryOptions(input, overrides?) |
Returns { queryKey, queryFn, ...options } for createQuery |
.infiniteQueryOptions(input, opts, overrides?) |
Returns options for createInfiniteQuery |
.mutationOptions(overrides?) |
Returns { mutationKey, mutationFn, ...options } for createMutation |
.mutation(overrides?) |
Returns a stable () => mutationOptions accessor for adapters expecting an options factory |
.queryKey(input?) |
Returns the query key |
.mutationKey(input?) |
Returns the mutation key |
.invalidate(queryClient, input?, exact?) |
Invalidates matching queries |
.prefetch(queryClient, input) |
Prefetch a query |
.ensureData(queryClient, input) |
Ensure data exists or fetch it |
.setData(queryClient, input, updater) |
Manually set cache data |
.getData(queryClient, input) |
Read from cache |
Query keys are deterministic and include routing information:
[
...queryKeyPrefix, // default ['eden']
method, // 'get', 'post', ...
pathTemplate, // e.g. ['users', ':id']
params ?? null,
query ?? null
]
You can pass standard TanStack Query options as overrides:
eden.posts.get.queryOptions(
{ query: { limit: "10" } },
{
staleTime: 5000,
gcTime: 10000,
enabled: isReady,
refetchOnMount: false,
retry: 3,
},
);eden.users.post.mutationOptions({
onMutate: (variables) => {
// Optimistic update
},
onSuccess: (data, variables) => {
// Invalidate related queries
},
onError: (error, variables, context) => {
// Rollback
},
});mutationOptions() and mutation() are equivalent in typing.
Use mutation() when your adapter usage prefers passing a stable options accessor directly.
If your API has many routes:
- Prefer
createEdenTQFromSchema<App['~Routes']>()in frontend/client packages. - Create feature-scoped route schema aliases (for example
BillingRoutes,CaseRoutes) and instantiate smaller clients per feature. - Increase tsserver memory in VS Code:
"typescript.tsserver.maxTsServerMemory": 4096
export function createUserQuery(userId: string) {
return createQuery<User>(() => ({
queryKey: ["users", userId],
queryFn: async () => {
const { data, error } = await api.users({ id: userId }).get();
if (error) throw error;
return data as User; // Manual cast!
},
}));
}export function createUserQuery(userId: string) {
return createQuery(() =>
eden.users({ id: userId }).get.queryOptions({
params: { id: userId },
}),
);
}
// Types are inferred from your Elysia server!MIT