Install
Add the package to both your backend and frontend.
The backend needs no extra peer deps — keyv is bundled. The frontend needs @tanstack/react-query, @tanstack/react-router, and react as peer deps.
Not using TanStack Start?
query-tags works with any Node.js or Bun backend. Run your Express, Hono, Fastify, Elysia, or Koa server alongside a Vite SPA — no TanStack Start required.
How it works
The backend mounts an adapter at /api/invalidator that exposes both an SSE stream (GET) and a control endpoint (POST). The SPA connects with createHTTPInvalidatorClient and receives invalidation events over SSE. When a mutation calls system.invalidateTags(…), every tagged query in the SPA refetches automatically.
Add the package to both your backend and frontend.
The backend needs no extra peer deps — keyv is bundled. The frontend needs @tanstack/react-query, @tanstack/react-router, and react as peer deps.
Create the tag invalidation system and mount the adapter. The only line that differs between frameworks is the adapter import.
import express from "express"
import cors from "cors"
import { createTagInvalidationSystem } from "@tanstack-tools/query-tags/server"
import { createExpressMiddleware } from "@tanstack-tools/query-tags/adapters/express"
const system = createTagInvalidationSystem()
const app = express()
app.use(cors())
app.use(express.json())
// Mount the invalidation API — handles both SSE stream and control endpoints
app.use("/api/invalidator", createExpressMiddleware(system))
// Call invalidateTags in any mutation handler
app.post("/api/todos", async (req, res) => {
const todo = await db.createTodo(req.body)
await system.invalidateTags({ tags: [["todos"]] })
res.status(201).json(todo)
})
app.listen(3001)Call system.invalidateTags({ tags: [...] }) in any mutation route and connected SPA clients will automatically refetch matching queries.
Proxy /api to the backend during development so the SPA and server share the same origin.
// vite.config.ts
import { defineConfig } from "vite"
export default defineConfig({
server: {
proxy: {
"/api": {
target: process.env.VITE_API_URL || "http://localhost:3001",
changeOrigin: true,
},
},
},
})Use createHTTPInvalidatorClient instead of the TanStack Start server-function variant.
// src/lib/invalidator-client.ts
import { createHTTPInvalidatorClient } from "@tanstack-tools/query-tags/http-client"
export const invalidatorClient = createHTTPInvalidatorClient({
basePath: "/api/invalidator",
})Import from @tanstack-tools/query-tags/http-client, not from /client — the /client entry requires TanStack Start server functions.
Add TagInvalidationProvider in your root route. It opens the SSE connection and watches active queries for matching tags.
// src/routes/__root.tsx
import { Outlet, createRootRoute, useRouter } from "@tanstack/react-router"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { TagInvalidationProvider } from "@tanstack-tools/query-tags/react"
import { invalidatorClient } from "../lib/invalidator-client"
const queryClient = new QueryClient()
export const Route = createRootRoute({ component: Root })
function Root() {
const router = useRouter()
return (
<QueryClientProvider client={queryClient}>
<TagInvalidationProvider
client={invalidatorClient}
queryClient={queryClient}
router={router}
>
<Outlet />
</TagInvalidationProvider>
</QueryClientProvider>
)
}Use defineTags to create a typed tag tree, then attach tags to queries via withTags or meta.tags. The provider picks them up automatically.
// src/lib/tags.ts
import { defineTags } from "@tanstack-tools/query-tags"
export const appTags = defineTags({
todos: {
list: () => ["todos"],
byId: (id: number) => ["todo", id],
},
})
// src/routes/index.tsx
import { useQuery } from "@tanstack/react-query"
import { withTags } from "@tanstack-tools/query-tags"
import { appTags } from "../lib/tags"
export function useTodos() {
return useQuery(
withTags(
{ queryKey: ["todos"], queryFn: fetchTodos },
[appTags.todos.list()],
),
)
}The repository contains a ready-to-run SPA frontend that works with all five backends:
Start any backend with pnpm --filter @tanstack-tools/example-server-express dev, then run the SPA with pnpm --filter @tanstack-tools/example-spa dev.