Skip to content
qt

Quick start

From zero to tag-based invalidation

Pick your stack below and follow the steps.

Prerequisites

Node.js 18+ or BunExisting REST API@tanstack/react-query v5+@tanstack/react-router v1+react 19+

How it works

The backend mounts an adapter at /api/invalidator that exposes both an SSE stream and a control endpoint. 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.

1

Install

Add the package to both your backend and frontend.

pnpm add @tanstack-tools/query-tags

The backend needs no extra peer deps — keyv is bundled. The frontend needs @tanstack/react-query, @tanstack/react-router, and react as peer deps.

2

Backend — mount the adapter

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.

3

Frontend — Vite proxy (dev)

Proxy /api to the backend during development so the SPA and server share the same origin.

vite.config.tsTS
// vite.config.ts
import { defineConfig } from "vite"

export default defineConfig({
  server: {
    proxy: {
      "/api": {
        target: process.env.VITE_API_URL || "http://localhost:3001",
        changeOrigin: true,
      },
    },
  },
})
4

Frontend — create the invalidator client

Use createHTTPInvalidatorClient instead of the TanStack Start server-function variant.

src/lib/invalidator-client.tsTS
// 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.

5

Frontend — wrap with the provider

Add TagInvalidationProvider in your root route. It opens the SSE connection and watches active queries for matching tags.

src/routes/__root.tsxTSX
// 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>
  )
}
6

Tag your queries

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()],
    ),
  )
}
Runnable examples

The repository contains a ready-to-run SPA frontend that works with all four 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.

Demos

See it working end-to-end

The live demos cover everything from basic tagged queries to multi-tab sync, scopes, and advanced patterns.

Explore demos →