Skip to content

Client Package

@veloxts/client provides a type-safe client for calling VeloxTS APIs from React applications.

Terminal window
pnpm add @veloxts/client @tanstack/react-query
lib/api.ts
import { createVeloxHooks, VeloxProvider } from '@veloxts/client/react';
import type { AppRouter } from '@/api';
// Create typed hooks with tRPC-style API
export const api = createVeloxHooks<AppRouter>();
// app/providers.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { VeloxProvider } from '@veloxts/client/react';
import type { AppRouter } from '@/api';
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<VeloxProvider<AppRouter> config={{ baseUrl: '/api' }}>
{children}
</VeloxProvider>
</QueryClientProvider>
);
}

With createVeloxHooks, you get tRPC-style hooks with full autocomplete:

import { api } from '@/lib/api';
function UserList() {
// Full autocomplete: api.users.listUsers.useQuery()
const { data: users, isLoading, error } = api.users.listUsers.useQuery();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
import { api } from '@/lib/api';
function CreateUserForm() {
const { mutate, isPending } = api.users.createUser.useMutation({
onSuccess: () => {
// Invalidate the users list to refetch
api.users.listUsers.invalidate();
},
});
return (
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
mutate({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
}}>
<input name="name" required />
<input name="email" type="email" required />
<button disabled={isPending}>
{isPending ? 'Creating...' : 'Create User'}
</button>
</form>
);
}
function UserProfile({ userId }: { userId: string }) {
const { data: user } = api.users.getUser.useQuery({ id: userId });
return user ? <h1>{user.name}</h1> : null;
}
function DeleteUserButton({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const { mutate } = api.users.deleteUser.useMutation({
onMutate: async ({ id }) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['users', 'listUsers'] });
// Snapshot previous value
const previous = queryClient.getQueryData(['users', 'listUsers']);
// Optimistically remove user
queryClient.setQueryData(['users', 'listUsers'], (old: User[]) =>
old?.filter(u => u.id !== id)
);
return { previous };
},
onError: (err, variables, context) => {
// Rollback on error
queryClient.setQueryData(['users', 'listUsers'], context?.previous);
},
onSettled: () => {
// Refetch after mutation
api.users.listUsers.invalidate();
},
});
return <button onClick={() => mutate({ id: userId })}>Delete</button>;
}
import { isVeloxClientError } from '@veloxts/client';
try {
await api.users.getUser({ id: 'invalid' });
} catch (error) {
if (isVeloxClientError(error)) {
switch (error.code) {
case 'NOT_FOUND':
console.log('User not found');
break;
case 'VALIDATION_ERROR':
console.log('Invalid input:', error.details);
break;
case 'UNAUTHORIZED':
// Redirect to login
break;
}
}
}

Types flow automatically from backend to frontend:

api/procedures/users.ts
// Backend defines the procedure
export const userProcedures = procedures('users', {
getUser: procedure()
.input(z.object({ id: z.string().uuid() }))
.output(UserSchema)
.query(handler),
});
// Frontend gets full type safety
// Input is typed: { id: string }
// Output is typed: User
const user = await api.users.getUser({ id: '123' });
// ^? User
// TypeScript catches errors
await api.users.getUser({ id: 123 }); // Error: Expected string