@nestjs-rpc/query
v1.0.1
Published
<div align="center">
Maintainers
Readme
@nestjs-rpc/query
Type-safe React Query hooks for NestRPC. Seamlessly integrate your RPC endpoints with TanStack Query for powerful caching, synchronization, and state management.
Installation • Quick Start • Examples
📖 📚 Full Documentation → 📖
Complete guides, API reference, and advanced examples
🎯 Why @nestjs-rpc/query?
Stop managing API state manually. This package gives you:
- 🔒 End-to-End Type Safety - Full TypeScript inference from RPC methods to React hooks
- ⚡ Automatic Caching - Built on TanStack Query with intelligent cache management
- 🔄 Auto Invalidation - Automatically invalidate related queries after mutations
- 🎯 Zero Boilerplate - No manual query key management or cache invalidation logic
- 🧩 Factory Pattern - Create reusable hooks with default options
- 📤 File Upload Support - Works seamlessly with RPC file uploads
The Traditional Way (Without @nestjs-rpc/query)
// ❌ Manual React Query setup with no type safety
const { data } = useQuery({
queryKey: ["user", userId],
queryFn: async () => {
const res = await fetch(`/api/user/${userId}`);
return res.json(); // 😱 No types!
},
});
// ❌ Manual cache invalidation
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries(["users"]); // 😱 Easy to forget!
},
});The NestRPC Query Way
// ✅ Type-safe, automatic query key generation
const { data } = useRpcQuery(rpc.user.getUserById, { id: "123" });
// ^? { id: string; name: string; email: string }
// Full autocomplete and type checking! 🎉
// ✅ Automatic cache invalidation
const mutation = useRpcMutation(rpc.user.createUser, {
invalidate: [rpc.user.listUsers], // Auto-invalidates after success!
});
// ✅ Type-safe mutation calls with body wrapper
mutation.mutate({ body: { name: "John", email: "[email protected]" } });📦 Installation
npm install @nestjs-rpc/query @nestjs-rpc/client @tanstack/react-query react
# or
pnpm add @nestjs-rpc/query @nestjs-rpc/client @tanstack/react-query react
# or
yarn add @nestjs-rpc/query @nestjs-rpc/client @tanstack/react-query reactPeer Dependencies:
@nestjs-rpc/client- The RPC client libraryreact- React 18.0.0 or higher@tanstack/react-query- React Query 5.0.0 or higher
🚀 Quick Start
1. Setup QueryClientProvider
Wrap your app with QueryClientProvider:
// App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RpcClient } from '@nestjs-rpc/client';
import type { Manifest } from './path-to-your-manifest';
// Create your RPC client
const rpcClient = new RpcClient<Manifest>({
baseUrl: 'http://localhost:3000',
});
// Create React Query client
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Your app components */}
</QueryClientProvider>
);
}2. Use RPC Queries
import { useRpcQuery } from '@nestjs-rpc/query';
import { rpc } from './rpc-client';
function UserList() {
const { data, isLoading, error } = useRpcQuery(
rpc.user.listUsers,
undefined, // body (optional for methods with no body)
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}3. Use RPC Mutations
import { useRpcMutation } from '@nestjs-rpc/query';
import { rpc } from './rpc-client';
function CreateUser() {
const mutation = useRpcMutation(rpc.user.createUser, {
invalidate: [rpc.user.listUsers], // Auto-invalidate after success
onSuccess: (data) => {
console.log('User created:', data);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate({
body: {
name: 'John Doe',
email: '[email protected]',
},
});
};
return (
<form onSubmit={handleSubmit}>
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create User'}
</button>
</form>
);
}That's it! You get:
- ✅ Full TypeScript autocomplete
- ✅ Automatic query key generation
- ✅ Intelligent caching
- ✅ Automatic cache invalidation
📚 Core API
useRpcQuery
Direct hook for one-off RPC queries. Automatically handles query key generation, caching, and type inference.
function useRpcQuery<TBody, TRet, TData = TRet>(
method: RpcMethod<TBody, TRet> & { [PathSymbol]: string[] },
body: TBody,
options?: RpcQueryOptions<UseQueryOptions<TRet, AxiosError, TData, QueryKey>>,
queryClient?: QueryClient,
): UseQueryResult<TData, AxiosError>;Example:
// With required body
const { data } = useRpcQuery(
rpc.user.getUserById,
{ id: "123" },
{
staleTime: 5000,
enabled: !!userId,
},
);
// With optional body (method accepts void/never/undefined)
const { data } = useRpcQuery(rpc.user.listUsers, undefined, {
refetchInterval: 30000,
});createRpcQuery
Factory function to create reusable query hooks with default options. Perfect for creating domain-specific hooks.
function createRpcQuery<TBody, TRet, TData = TRet>(
method: RpcMethod<TBody, TRet> & { [PathSymbol]: string[] },
defaultOptions?: RpcQueryOptions<UseQueryOptions<TRet, AxiosError, TData>>,
): RpcQueryHook<TBody, TRet, TData>;Example:
// Create a reusable hook with defaults
const useUserList = createRpcQuery(rpc.user.listUsers, {
staleTime: 60000,
refetchOnWindowFocus: false,
});
// Use it in components
function UserList() {
const { data } = useUserList(undefined);
// ...
}
// Override options per usage
function AnotherComponent() {
const { data } = useUserList(undefined, {
refetchInterval: 10000, // Override default
});
}useRpcMutation
Direct hook for one-off RPC mutations. Supports automatic cache invalidation.
function useRpcMutation<TBody, TRet, TOnMutateResult = unknown>(
method: RpcMethod<TBody, TRet> & { [PathSymbol]: string[] },
options?: RpcMutationOptions<TBody, TRet, TOnMutateResult>,
): UseMutationResult<TRet, AxiosError, RpcMutationBody<TBody>, TOnMutateResult>;Example:
const mutation = useRpcMutation(rpc.user.createUser, {
invalidate: [rpc.user.listUsers],
onSuccess: (data) => {
toast.success("User created!");
},
onError: (error) => {
toast.error(error.message);
},
});
// Pass body directly in mutation variables
mutation.mutate({ body: { name: "John", email: "[email protected]" } });createRpcMutation
Factory function to create reusable mutation hooks with default options.
function createRpcMutation<TBody, TRet, TOnMutateResult = unknown>(
method: RpcMethod<TBody, TRet> & { [PathSymbol]: string[] },
defaultOptions?: RpcMutationOptions<TBody, TRet, TOnMutateResult>,
): (
options?: RpcMutationOptions<TBody, TRet, TOnMutateResultOverride>,
) => UseMutationResult<
TRet,
AxiosError,
RpcMutationBody<TBody>,
TOnMutateResultOverride
>;Example:
// Create reusable mutation hook
const useCreateUser = createRpcMutation(rpc.user.createUser, {
invalidate: [rpc.user.listUsers],
onSuccess: () => {
console.log('User created successfully');
},
});
// Use in components
function CreateUserForm() {
const createUser = useCreateUser({
onSuccess: (data) => {
// Additional per-component logic
navigate(`/users/${data.id}`);
},
});
return (
<button onClick={() => createUser.mutate({ body: { name: 'John', email: '[email protected]' } })}>
Create
</button>
);
}useInvalidateRpcQuery
Hook that returns a function to invalidate queries by RPC route. Useful for manual cache invalidation.
function useInvalidateRpcQuery(): (
rpcRoute: { [PathSymbol]: string[] },
filters?: Omit<InvalidateQueryFilters<QueryKey>, "queryKey">,
options?: InvalidateOptions,
) => Promise<void>;Example:
function UserActions() {
const invalidate = useInvalidateRpcQuery();
const handleRefresh = () => {
invalidate(rpc.user.listUsers);
};
return <button onClick={handleRefresh}>Refresh Users</button>;
}🎨 Advanced Features
Additional Query Keys
Extend query keys for more granular cache control. Useful when you need to differentiate queries with the same body but different contexts.
// Using array
const { data } = useRpcQuery(
rpc.user.getUserById,
{ id: "123" },
{
useAdditionalQueryKey: ["admin-view"], // Adds to query key
},
);
// Using function (allows using hooks inside)
const { data } = useRpcQuery(
rpc.user.getUserById,
{ id: "123" },
{
useAdditionalQueryKey: () => {
const { userRole } = useAuth(); // Can use hooks!
return [userRole];
},
},
);
// In factory defaults
const useUserById = createRpcQuery(rpc.user.getUserById, {
useAdditionalQueryKey: ["default"],
});
// Instance-level keys are merged with factory defaults
const { data } = useUserById(
{ id: "123" },
{
useAdditionalQueryKey: ["instance"], // Final key: ['default', 'instance']
},
);File Uploads & RPC Options
Pass files and RPC options directly in the mutation variables:
// Single file upload
const mutation = useRpcMutation(rpc.files.uploadFile, {
onSuccess: () => console.log("Uploaded!"),
});
mutation.mutate({
body: { description: "My file" },
file: fileInput.files[0],
});
// Multiple files
mutation.mutate({
body: { category: "documents" },
files: Array.from(fileInput.files),
});
// With custom RPC options (headers, axios instance, etc)
mutation.mutate({
body: { description: "My file" },
file: fileInput.files[0],
rpcOptions: {
requestOptions: {
headers: { "X-Custom-Header": "value" },
},
},
});
// Static RPC options (applied to all mutations from this hook)
const useUploadFile = createRpcMutation(rpc.files.uploadFile, {
invalidate: [rpc.files.listFiles],
rpcOptions: {
requestOptions: { timeout: 30000 },
},
});
// Dynamic options override static ones
const uploadFile = useUploadFile();
uploadFile.mutate({
body: { description: "My file" },
file: fileInput.files[0],
rpcOptions: {
requestOptions: { timeout: 60000 }, // Overrides the 30000 from hook
},
});Automatic Cache Invalidation
Mutations can automatically invalidate related queries:
const useCreateUser = createRpcMutation(rpc.user.createUser, {
invalidate: [
rpc.user.listUsers, // Invalidate list
rpc.user.getUserById, // Invalidate detail queries
],
});
// After successful mutation, all queries for these routes are invalidatedCustom Query Client
Pass a custom QueryClient instance for isolated query management:
const customQueryClient = new QueryClient();
const { data } = useRpcQuery(
rpc.user.listUsers,
undefined,
{},
customQueryClient, // Use specific client
);💡 Complete Example
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RpcClient } from '@nestjs-rpc/client';
import {
createRpcQuery,
createRpcMutation,
useInvalidateRpcQuery,
} from '@nestjs-rpc/query';
import type { Manifest } from './nest-rpc.config';
// Setup
const rpcClient = new RpcClient<Manifest>({
baseUrl: 'http://localhost:3000',
});
const rpc = rpcClient.routers();
const queryClient = new QueryClient();
// Create reusable hooks
const useUserList = createRpcQuery(rpc.user.listUsers, {
staleTime: 60000,
});
const useCreateUser = createRpcMutation(rpc.user.createUser, {
invalidate: [rpc.user.listUsers],
});
// Component
function UserManagement() {
const { data: users, isLoading } = useUserList(undefined);
const createUser = useCreateUser({
onSuccess: () => {
toast.success('User created!');
},
});
const invalidate = useInvalidateRpcQuery();
const handleCreate = () => {
createUser.mutate({
body: {
name: 'John Doe',
email: '[email protected]',
},
});
};
const handleRefresh = () => {
invalidate(rpc.user.listUsers);
};
return (
<div>
<button onClick={handleCreate} disabled={createUser.isPending}>
Create User
</button>
<button onClick={handleRefresh}>Refresh</button>
{isLoading ? (
<div>Loading...</div>
) : (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
}
// App
function App() {
return (
<QueryClientProvider client={queryClient}>
<UserManagement />
</QueryClientProvider>
);
}🔒 Type Safety
The hooks automatically infer types from your RPC manifest:
// Server method signature:
@Route()
async getUserById(id: string): Promise<User> {
return { id, name: 'John', email: '[email protected]' };
}
// Client automatically gets:
const { data } = useRpcQuery(rpc.user.getUserById, { id: '123' });
// ^? { data: User }
// ^? body parameter is typed as { id: string }
// ^? Full autocomplete for User propertiesIf your server types change, your client code will show TypeScript errors immediately!
🎯 Best Practices
- Use factory pattern - Create reusable hooks with
createRpcQueryandcreateRpcMutation - Auto-invalidate - Use
invalidateoption in mutations to keep cache fresh - Centralize RPC client - Create one RPC client instance and export it
- Handle errors - Use
onErrorcallbacks or error boundaries - Use additional keys - Extend query keys when you need context-specific caching
📖 API Reference
Query Options
All standard React Query options are supported, except:
queryKey- Automatically generatedqueryFn- Automatically generatedmeta- Automatically generated
Mutation Options
All standard React Query mutation options are supported, plus:
invalidate?: { [PathSymbol]: string[] }[]- Routes to invalidate after successrpcOptions?: Omit<RpcMethodOptions, "file" | "files">- Static RPC options applied to all mutations (can be overridden per call)
Mutation Variables
Mutations accept RpcMutationBody<TBody> which includes:
body: TBody- The request body (required if TBody is required, optional otherwise)file?: File- Single file to uploadfiles?: File[]- Multiple files to uploadrpcOptions?: Omit<RpcMethodOptions, "file" | "files">- Per-call RPC options (overrides static options)
Query Key Structure
Query keys are automatically generated as:
[...path, body, rpcOptions, ...additionalKeys];This ensures proper cache differentiation based on all parameters.
💡 Examples
Check out the example directory for a complete working example with React.
📚 Need More Help?
📖 Full Documentation →
Complete guides, API reference, advanced patterns, and troubleshooting
🔗 Related
@nestjs-rpc/client- The RPC client library@nestjs-rpc/server- The server-side library- TanStack Query Docs - React Query documentation
🤝 Contributing
Contributions welcome! See the main README for details.
📄 License
MIT
Made with ❤️ for the NestJS community
