A server-side data-fetching library inspired by TanStack Query, designed for caching and managing database queries on the server with support for multiple scoping strategies and pluggable cache backends.
- 🚀 Simple, declarative API for server-side data fetching
- 💾 Pluggable caching system with in-memory and Redis adapters
- 🔄 Automatic retries with exponential backoff
- ⚡ Request deduplication to prevent duplicate database calls
- 🎯 Multiple scoping strategies: request, session, and global
- 🔄 Query invalidation with exact and partial matching
- 🏗️ Database agnostic - works with any ORM or database driver
- 📊 Built-in mutation support with lifecycle callbacks
- 🧪 TypeScript first with full type safety
- 🛠️ Monorepo structure with dedicated adapters
# Core library
bun install cacheq
# Optional: Redis cache adapter
bun install @cacheq/redis-adapter
# Optional: Drizzle ORM adapter
bun install @cacheq/drizzle-adapterimport { QueryClient } from 'cacheq';
// Create a new client for each request
const queryClient = new QueryClient();
// Define your data fetcher
const fetchUser = async ({ queryKey }) => {
const [, userId] = queryKey;
return await db.select().from(users).where(eq(users.id, userId));
};
// Create and execute query
const userQuery = queryClient.createQuery(['user', 1], fetchUser);
// Access query state
console.log(userQuery.status); // 'loading' | 'success' | 'error'
console.log(userQuery.data); // User data when loaded
console.log(userQuery.error); // Error if failedimport { QueryClientManager } from 'cacheq';
import { createRedisCache } from '@cacheq/redis-adapter';
// Set up distributed cache
const redisCache = createRedisCache({
redis: { url: 'redis://localhost:6379' },
keyPrefix: 'myapp:',
defaultTTL: 600 // 10 minutes
});
// Create manager for session-scoped clients
const manager = new QueryClientManager({
cache: redisCache,
defaultCacheTime: 10 * 60 * 1000
});
// Get client for specific session (persists across requests)
const sessionClient = manager.getClient({ sessionId: 'user-123' });
const userPrefs = sessionClient.createQuery(['preferences', 'user-123'], fetchPreferences);// For data shared across all users
const globalClient = manager.getGlobalClient();
const appConfig = globalClient.createQuery(['app-config'], fetchAppConfig);const createUserMutation = queryClient.createMutation({
mutationFn: async (userData) => {
return await db.insert(users).values(userData);
},
onSuccess: (data, variables) => {
// Invalidate users list to refetch
queryClient.invalidateQueries({ queryKey: ['users'] });
},
onError: (error, variables) => {
console.error('Failed to create user:', error);
}
});
// Execute mutation
await createUserMutation({ name: 'John', email: '[email protected]' });The main class for managing queries and mutations.
const queryClient = new QueryClient({
cache?: Cache; // Custom cache implementation
defaultCacheTime?: number; // Default TTL in milliseconds
});createQuery<T>(queryKey, fetcher, options?)- Create a querycreateMutation<T>(options)- Create a mutationinvalidateQueries(options)- Invalidate and refetch queriessetQueryData(queryKey, data)- Manually set query datagetQueryData(queryKey)- Get cached query dataprefetchQuery(queryKey, fetcher)- Prefetch dataclear()- Clear all cached data
Manages multiple QueryClient instances for session and global scoping.
const manager = new QueryClientManager({
cache: Cache; // Distributed cache implementation
defaultCacheTime?: number; // Default TTL in milliseconds
});getClient({ sessionId })- Get session-scoped clientgetGlobalClient()- Get global singleton clientremoveSession(sessionId)- Clean up sessionclearAllSessions()- Clear all session datagetSessionCount()- Get active session count
Implement this interface to create custom cache adapters:
interface Cache {
get<T>(key: string): Promise<T | undefined>;
set(key: string, value: any, ttl?: number): Promise<void>;
delete(key: string): Promise<void>;
clear(): Promise<void>;
}interface QueryOptions {
retry?: number; // Number of retries (default: 3)
retryDelay?: (attemptIndex: number) => number; // Retry delay function
cacheTime?: number; // Cache TTL in milliseconds
}import express from 'express';
import { QueryClient } from 'cacheq';
const app = express();
// Middleware to create QueryClient per request
app.use((req, res, next) => {
req.queryClient = new QueryClient();
next();
});
app.get('/users/:id', async (req, res) => {
const userQuery = req.queryClient.createQuery(
['user', req.params.id],
fetchUser
);
// Wait for query to complete and respond
res.json({ user: userQuery.data });
});import Fastify from 'fastify';
import { QueryClient } from 'cacheq';
const fastify = Fastify();
// Plugin to add QueryClient to request
fastify.addHook('onRequest', async (request) => {
request.queryClient = new QueryClient();
});
fastify.get('/users/:id', async (request) => {
const userQuery = request.queryClient.createQuery(
['user', request.params.id],
fetchUser
);
return { user: userQuery.data };
});This is a monorepo containing several packages:
The main library with QueryClient, QueryClientManager, and core functionality.
Redis cache adapter for distributed caching across multiple server instances.
Integration helpers for Drizzle ORM to simplify query creation.
# Install dependencies
bun install
# Run tests
bun test
# Build all packages
bun run build
# Test specific package
cd packages/core && bun test- ✅ Core query and mutation functionality
- ✅ Pluggable cache system with Redis adapter
- ✅ Session and global scoping via QueryClientManager
- ✅ Automatic retries and request deduplication
- ✅ Query invalidation with partial matching
- 🚧 Stale-while-revalidate functionality
- 🚧 Optimistic updates for mutations
- 🚧 Enhanced Drizzle ORM integration
- 🚧 DevTools for debugging and inspection
- 🚧 Additional cache adapters (Memcached, DynamoDB)
Contributions are welcome! Please read our contributing guidelines and submit pull requests to the main branch.
MIT License - see LICENSE file for details.