Getting Started
- @ductape/client
- React
- Vue 3
The @ductape/client package is the core JavaScript/TypeScript SDK for Ductape. It works with any framework or vanilla JavaScript and provides a simple, type-safe interface to all Ductape services.
Installation
npm install @ductape/client
# or
yarn add @ductape/client
# or
pnpm add @ductape/client
Basic Setup
In browser or any frontend code, never use accessKey. Use a publishable key and point the client at your Ductape proxy (BFF). The proxy resolves the key and enforces scope. Get your publishable key from Workbench → Tokens → Publishable Key.
When using a publishable key, every request (e.g. databases.query, storage.upload, actions.run) must include a session property in the options object—a session token issued by your backend. Session methods (sessions.start, sessions.verify, etc.) are not available from the client with a publishable key; they throw. Sessions can only be started and managed on the backend. See Sessions & Authentication and the main docs on frontend access key strategies.
1. Initialize the Client
import { Ductape } from '@ductape/client';
const ductape = new Ductape({
publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
});
2. Connect for Real-time Features
If you need real-time subscriptions or WebSocket features, establish a connection:
await ductape.connect();
console.log('Connected to Ductape!');
The connection is optional. You can use databases, storage, and other services over HTTP without calling connect(). Only call it if you need real-time subscriptions or presence features.
3. Use Services
const sessionToken = getSessionFromYourBackend(); // from your auth
// Database operations (include session in every request when using publishable key)
await ductape.databases.connect({ database: 'main', session: sessionToken });
const users = await ductape.databases.query({
table: 'users',
where: { active: true },
limit: 10,
session: sessionToken,
});
console.log('Active users:', users.rows);
Configuration (frontend)
In frontend code you only need publishableKey. Never use accessKey in the browser.
interface IDuctapeClientConfig {
publishableKey: string; // From Workbench → Tokens → Publishable Key
}
Example
const ductape = new Ductape({
publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
});
Get your publishable key from the Workbench (Tokens → Publishable Key). Configure scope there to control which module/methods the key can call.
accessKey is for server-side or Node only (e.g. BFF, scripts). Do not use it in frontend or browser code.
Quick Examples
Database Query
const sessionToken = getSessionFromYourBackend();
const result = await ductape.databases.query({
table: 'products',
where: {
category: 'electronics',
price: { $lt: 1000 }
},
orderBy: [{ column: 'price', order: 'asc' }],
limit: 20,
session: sessionToken,
});
console.log(`Found ${result.count} products`);
Insert Data
const sessionToken = getSessionFromYourBackend();
const newUser = await ductape.databases.insert({
table: 'users',
data: {
name: 'Alice Johnson',
email: 'alice@example.com',
role: 'user'
},
session: sessionToken,
});
console.log('Created user:', newUser.rows[0]);
Upload a File
const sessionToken = getSessionFromYourBackend();
const file = document.getElementById('fileInput').files[0];
const result = await ductape.storage.upload({
file: file,
path: 'uploads/documents',
session: sessionToken,
onProgress: (progress) => {
console.log(`Upload progress: ${progress.percentage}%`);
}
});
console.log('File uploaded:', result.url);
Execute a Feature
const sessionToken = getSessionFromYourBackend();
const execution = await ductape.features.execute({
feature: 'process-order',
input: {
orderId: '12345',
userId: 'user-789'
},
session: sessionToken,
});
console.log('Feature execution:', execution.executionId);
Subscribe to Database Changes
const sessionToken = getSessionFromYourBackend();
await ductape.connect(); // WebSocket connection required
const subscription = ductape.databases.subscribe({
table: 'messages',
where: { channel: 'general' },
session: sessionToken,
onChange: (event) => {
console.log('Change detected:', event.type, event.data);
if (event.type === 'insert') {
console.log('New message:', event.data.new);
}
}
});
// Later: unsubscribe
subscription.unsubscribe();
Connection State Management
Monitor the connection state for real-time features:
await ductape.connect();
ductape.onConnectionStateChange((state) => {
console.log('Connection state:', state);
// States: 'connected', 'disconnected', 'connecting', 'reconnecting'
});
Error Handling
All async operations throw errors that you should handle:
const sessionToken = getSessionFromYourBackend();
try {
const result = await ductape.databases.query({
table: 'users',
where: { id: userId },
session: sessionToken,
});
console.log('User:', result.rows[0]);
} catch (error) {
console.error('Query failed:', error.message);
// Error types you might encounter:
// - Network errors
// - Authentication errors
// - Validation errors
// - Not found errors
}
TypeScript Support
The client is fully typed. You can provide type parameters for better intellisense:
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
const sessionToken = getSessionFromYourBackend();
const result = await ductape.databases.query<User>({
table: 'users',
limit: 10,
session: sessionToken,
});
// result.rows is typed as User[]
const firstUser: User = result.rows[0];
Using with Different Frameworks
Vanilla JavaScript
<!DOCTYPE html>
<html>
<head>
<script type="module">
import { Ductape } from 'https://cdn.jsdelivr.net/npm/@ductape/client/+esm';
const ductape = new Ductape({
publishableKey: 'your-publishable-key',
});
async function loadUsers() {
const sessionToken = getSessionFromYourBackend(); // from your auth
const result = await ductape.databases.query({
table: 'users',
limit: 10,
session: sessionToken,
});
document.getElementById('users').innerHTML = result.rows
.map(u => `<li>${u.name}</li>`)
.join('');
}
loadUsers();
</script>
</head>
<body>
<ul id="users"></ul>
</body>
</html>
With React (without hooks)
import { Ductape } from '@ductape/client';
import { useEffect, useState } from 'react';
const ductape = new Ductape({
publishableKey: process.env.REACT_APP_PUBLISHABLE_KEY,
});
function UsersList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadUsers() {
try {
const sessionToken = getSessionFromYourBackend();
const result = await ductape.databases.query({
table: 'users',
limit: 10,
session: sessionToken,
});
setUsers(result.rows);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}
loadUsers();
}, []);
if (loading) return <div>Loading...</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
For React applications, we recommend using @ductape/react which provides hooks and better integration.
With Vue (without composables)
<script>
import { Ductape } from '@ductape/client';
const ductape = new Ductape({
publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
});
export default {
data() {
return {
users: [],
loading: true
};
},
async mounted() {
try {
const sessionToken = getSessionFromYourBackend();
const result = await ductape.databases.query({
table: 'users',
limit: 10,
session: sessionToken,
});
this.users = result.rows;
} catch (error) {
console.error(error);
} finally {
this.loading = false;
}
}
};
</script>
<template>
<div v-if="loading">Loading...</div>
<ul v-else>
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
</template>
For Vue 3 applications, we recommend using @ductape/vue which provides composables and better integration.
Next Steps
Helper Functions
The package also exports a createClient helper:
import { createClient } from '@ductape/client';
const ductape = createClient({
publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
});
// Equivalent to: new Ductape({ ... })
The @ductape/react package provides React hooks and components for building real-time applications with Ductape. Built on top of @ductape/client, it offers a React-idiomatic way to work with databases, storage, features, and more.
Installation
npm install @ductape/react @ductape/client
# or
yarn add @ductape/react @ductape/client
# or
pnpm add @ductape/react @ductape/client
Both @ductape/react and @ductape/client are required. The React package is a peer dependency wrapper around the core client.
Requirements
- React 17.0.0 or higher
- TypeScript 4.5+ (optional but recommended)
Basic Setup
In browser/frontend never use accessKey. Use publishableKey and baseUrl (your Ductape proxy). Get your publishable key from Workbench → Tokens → Publishable Key.
The design is: a long-lived publishable key (safe in the frontend) and a short-lived session token per user (created on your backend). Your backend calls ductape.sessions.start() with your access key (e.g. at user login) and returns the session token to the client. The frontend then includes that token as session in every Ductape request. Session hooks (useSessionStart, useSessionVerify, etc.) are not available with a publishable key—sessions are created only on the backend. See Sessions (backend) and Session Hooks.
1. Create a config (publishable key, product, env)
Use a long-lived publishable key in your frontend config. The session token is not part of this config—it comes from your backend (e.g. login response) and must be passed in every request (step 2).
// config.ts
export const baseUrl = import.meta.env.VITE_BASE_URL || 'https://api.ductape.app';
export const publishableKey = import.meta.env.VITE_PUBLISHABLE_KEY || '';
export const product = import.meta.env.VITE_PRODUCT || 'ductape:rematch';
export const env = import.meta.env.VITE_ENV || 'snd';
2. Wrap Your App with DuctapeProvider
import { DuctapeProvider } from '@ductape/react';
import { publishableKey, product, env } from './config';
function App() {
return (
<DuctapeProvider
config={{
publishableKey,
product,
env,
}}
autoConnect={false}
>
<YourApp />
</DuctapeProvider>
);
}
export default App;
3. Get the session token from your backend and pass it to every request
Your backend creates a session (e.g. at login) with the access key and returns the token to the frontend:
// Backend (e.g. login handler) — use @ductape/sdk with accessKey
const ductape = new Ductape({ accessKey: process.env.DUCTAPE_ACCESS_KEY });
const { token } = await ductape.sessions.start({
product: 'your-product',
env: 'prd',
tag: 'user-session',
data: { userId: user.id, email: user.email },
});
// Return { token } to the client in the login response
The frontend receives that token (e.g. from your auth state or API) and passes it as session in every Ductape request:
// Frontend: get session from your auth (e.g. login response / auth context)
const sessionToken = useAuth().sessionToken; // or from state/context after login
4. Use Hooks in Your Components (include session in every request)
import { useDatabaseQuery } from '@ductape/react';
import { useAuth } from './auth'; // your auth context that holds the session token from backend
function UsersList() {
const { sessionToken } = useAuth();
const { data, isLoading, error, refetch } = useDatabaseQuery(
'users',
{
table: 'users',
where: { active: true },
limit: 10,
session: sessionToken,
}
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<button onClick={() => refetch()}>Refresh</button>
<ul>
{(data?.data ?? []).map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
Provider Configuration
The DuctapeProvider accepts these props:
interface DuctapeProviderProps {
config: {
publishableKey: string; // From Workbench → Tokens → Publishable Key
product?: string;
env?: string;
};
autoConnect?: boolean; // Auto-connect WebSocket (default: false)
children: React.ReactNode;
}
Session is not in the provider config. Pass the session token (from your backend) in the options for every hook call (e.g. useDatabaseQuery, useActionRun, useBrokerPublish, useMutation).
Environment Variables
Vite: use the VITE_ prefix so variables are exposed to the client. Only the publishable key and app config belong here. The session token must come from your backend (e.g. after login), not from env.
# .env (Vite)
VITE_PUBLISHABLE_KEY=dpk_xxx
VITE_PRODUCT=ductape:rematch
VITE_ENV=snd
// config.ts
export const publishableKey = import.meta.env.VITE_PUBLISHABLE_KEY || '';
export const product = import.meta.env.VITE_PRODUCT || 'ductape:rematch';
export const env = import.meta.env.VITE_ENV || 'snd';
For local development only, you may use a dev session token in env (e.g. VITE_SESSION=...) if your backend is not running; in production the session must always come from your backend.
Create React App: use REACT_APP_ prefix and process.env.REACT_APP_PUBLISHABLE_KEY, etc.
Quick Examples
In the examples below, sessionToken is the session token your frontend received from your backend (e.g. in the login response). Pass it in every request when using a publishable key.
Query Data
import { useDatabaseQuery } from '@ductape/react';
import { useAuth } from './auth'; // or wherever you store the backend-issued session token
function ProductList() {
const { sessionToken } = useAuth();
const { data, isLoading, error } = useDatabaseQuery(
'products',
{
table: 'products',
where: { category: 'electronics' },
orderBy: [{ column: 'price', order: 'asc' }],
limit: 20,
session: sessionToken,
}
);
if (isLoading) return <div>Loading products...</div>;
if (error) return <div>Error loading products</div>;
return (
<div>
{(data?.data ?? []).map((product: any) => (
<div key={product.id}>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
))}
</div>
);
}
Insert Data
import { useDatabaseInsert } from '@ductape/react';
import { useState } from 'react';
import { useAuth } from './auth';
function CreateUser() {
const { sessionToken } = useAuth();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const { mutate, isLoading, error } = useDatabaseInsert({
onSuccess: (data) => {
console.log('User created:', data);
setName('');
setEmail('');
}
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutate({
table: 'users',
data: { name, email },
session: sessionToken,
});
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
required
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
type="email"
required
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Creating...' : 'Create User'}
</button>
{error && <p>Error: {error.message}</p>}
</form>
);
}
Real-time Subscription
import { useDatabaseSubscription } from '@ductape/react';
import { useState } from 'react';
import { useAuth } from './auth';
function LiveMessages() {
const { sessionToken } = useAuth();
const [messages, setMessages] = useState([]);
useDatabaseSubscription({
table: 'messages',
where: { channel: 'general' },
session: sessionToken,
onChange: (event) => {
if (event.type === 'insert') {
setMessages(prev => [...prev, event.data.new]);
}
}
});
return (
<ul>
{messages.map(msg => (
<li key={msg.id}>{msg.text}</li>
))}
</ul>
);
}
File Upload
import { useUpload } from '@ductape/react';
import { useAuth } from './auth';
function FileUploader() {
const { sessionToken } = useAuth();
const { upload, progress, isLoading, error } = useUpload();
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
upload({
storage: 'gcp-storage',
fileName: `uploads/documents/${file.name}`,
data: file,
mimeType: file.type || 'application/octet-stream',
session: sessionToken,
});
}
};
return (
<div>
<input
type="file"
onChange={handleFileChange}
disabled={isLoading}
/>
{isLoading && <p>Uploading: {progress?.percentage}%</p>}
{error && <p>Error: {error.message}</p>}
</div>
);
}
Execute Feature
import { useFeatureExecute } from '@ductape/react';
import { useAuth } from './auth';
function ProcessOrder({ orderId }: { orderId: string }) {
const { sessionToken } = useAuth();
const { mutate, isLoading, data, error } = useFeatureExecute();
const handleProcess = () => {
mutate({
feature: 'process-order',
input: { orderId },
session: sessionToken,
});
};
return (
<div>
<button onClick={handleProcess} disabled={isLoading}>
{isLoading ? 'Processing...' : 'Process Order'}
</button>
{data && <p>Execution ID: {data.executionId}</p>}
{error && <p>Error: {error.message}</p>}
</div>
);
}
Accessing the Client Directly
You can access the underlying Ductape client instance. Include the session token (from your backend) in every call when using a publishable key.
import { useDuctape } from '@ductape/react';
import { useAuth } from './auth';
function MyComponent() {
const { sessionToken } = useAuth();
const { client, isReady, isConnected } = useDuctape();
const handleCustomOperation = async () => {
const result = await client.databases.query({
table: 'custom_table',
session: sessionToken,
});
};
return (
<div>
<p>Ready: {isReady ? 'Yes' : 'No'}</p>
<p>Connected: {isConnected ? 'Yes' : 'No'}</p>
</div>
);
}
TypeScript Support
The hooks are fully typed. You can provide type parameters:
import { useAuth } from './auth';
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
function UsersList() {
const { sessionToken } = useAuth();
const { data, isLoading } = useDatabaseQuery<User>(
'users',
{
table: 'users',
limit: 10,
session: sessionToken,
}
);
// data.data is typed as User[]
return (
<ul>
{(data?.data ?? []).map(user => (
<li key={user.id}>
{user.name} - {user.role}
</li>
))}
</ul>
);
}
Error Boundaries
Wrap your components with error boundaries to handle errors gracefully:
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<DuctapeProvider config={...}>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<YourApp />
</ErrorBoundary>
</DuctapeProvider>
);
}
Best Practices
-
Use query keys wisely: The first parameter to hooks like
useDatabaseQueryis a query key used for caching. Make it unique per query. -
Memoize query options: Use
useMemofor complex query options to prevent unnecessary re-renders:
import { useAuth } from './auth';
function UsersList() {
const { sessionToken } = useAuth();
const queryOptions = useMemo(
() => ({
table: 'users',
where: { active: true },
session: sessionToken,
}),
[sessionToken]
);
const { data } = useDatabaseQuery('users', queryOptions);
// ...
}
-
Handle loading and error states: Always provide UI feedback for loading and error states.
-
Cleanup subscriptions: Subscriptions automatically cleanup when components unmount.
-
Use TypeScript: Leverage TypeScript for type safety and better developer experience.
Next Steps
- Database Hooks
- Storage Hooks
- Feature Hooks
- Real-time Subscriptions
- Session Hooks (backend/access-key only; with publishable key, pass
sessionin every request)
The @ductape/vue package provides Vue 3 composables and a plugin for building real-time applications with Ductape. Built on top of @ductape/client, it offers a reactive, Vue-idiomatic way to work with databases, storage, features, and more.
Installation
npm install @ductape/vue @ductape/client
# or
yarn add @ductape/vue @ductape/client
# or
pnpm add @ductape/vue @ductape/client
Both @ductape/vue and @ductape/client are required. The Vue package is a peer dependency wrapper around the core client.
Requirements
- Vue 3.0.0 or higher
- TypeScript 4.5+ (optional but recommended)
Basic Setup
In browser/frontend never use accessKey. Use publishableKey and baseUrl (your Ductape proxy). Get your publishable key from Workbench → Tokens → Publishable Key.
When using a publishable key, every request must include a session property in the options/payload—a session token from your backend. Pass it in composable options, e.g. useDatabaseQuery(['key'], { table: 'users', session: sessionToken }) or in the payload for useActionRun, useMutation, etc. Session composables (useSessionStart, useSessionVerify, etc.) are not available with a publishable key—they throw. Sessions are backend-only; the frontend only passes the token in the session field. See Session Composables for the backend/access-key usage.
1. Install the Plugin
// main.ts
import { createApp } from 'vue';
import { createDuctape } from '@ductape/vue';
import App from './App.vue';
const app = createApp(App);
const ductape = createDuctape({
publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
autoConnect: false,
});
app.use(ductape);
app.mount('#app');
2. Use Composables in Your Components
<script setup lang="ts">
import { useDatabaseQuery } from '@ductape/vue';
const sessionToken = useSessionToken(); // from your auth
const { data, isLoading, error, refetch } = useDatabaseQuery(
['users'],
{
table: 'users',
where: { active: true },
limit: 10,
session: sessionToken,
}
);
</script>
<template>
<div>
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else>
<button @click="refetch">Refresh</button>
<ul>
<li v-for="user in data?.rows" :key="user.id">
{{ user.name }}
</li>
</ul>
</div>
</div>
</template>
Plugin Configuration
The createDuctape function accepts these options:
interface DuctapePluginOptions {
publishableKey: string; // From Workbench → Tokens → Publishable Key
autoConnect?: boolean; // Optional: Auto-connect WebSocket (default: false)
}
Environment Variables
# .env.local
VITE_PUBLISHABLE_KEY=dpk_xxx
// main.ts
const ductape = createDuctape({
publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
autoConnect: false,
});
app.use(ductape);
Quick Examples
Query Data
<script setup lang="ts">
import { useDatabaseQuery } from '@ductape/vue';
const sessionToken = useSessionToken();
const { data, isLoading, error } = useDatabaseQuery(
['products', 'electronics'],
{
table: 'products',
where: { category: 'electronics' },
orderBy: [{ column: 'price', order: 'asc' }],
limit: 20,
session: sessionToken,
}
);
</script>
<template>
<div>
<div v-if="isLoading">Loading products...</div>
<div v-else-if="error">Error loading products</div>
<div v-else>
<div v-for="product in data?.rows" :key="product.id">
<h3>{{ product.name }}</h3>
<p>${{ product.price }}</p>
</div>
</div>
</div>
</template>
Insert Data
<script setup lang="ts">
import { ref } from 'vue';
import { useDatabaseInsert } from '@ductape/vue';
const name = ref('');
const email = ref('');
const { mutate, isLoading, error } = useDatabaseInsert({
onSuccess: (data) => {
console.log('User created:', data.rows[0]);
name.value = '';
email.value = '';
}
});
const handleSubmit = () => {
mutate({
table: 'users',
data: {
name: name.value,
email: email.value
}
});
};
</script>
<template>
<form @submit.prevent="handleSubmit">
<input
v-model="name"
placeholder="Name"
required
/>
<input
v-model="email"
placeholder="Email"
type="email"
required
/>
<button type="submit" :disabled="isLoading">
{{ isLoading ? 'Creating...' : 'Create User' }}
</button>
<p v-if="error">Error: {{ error.message }}</p>
</form>
</template>
Real-time Subscription
<script setup lang="ts">
import { ref } from 'vue';
import { useDatabaseSubscription } from '@ductape/vue';
const messages = ref([]);
useDatabaseSubscription({
table: 'messages',
where: { channel: 'general' },
onChange: (event) => {
if (event.type === 'insert') {
messages.value.push(event.data.new);
}
}
});
</script>
<template>
<ul>
<li v-for="msg in messages" :key="msg.id">
{{ msg.text }}
</li>
</ul>
</template>
File Upload
<script setup lang="ts">
import { useUpload } from '@ductape/vue';
const { upload, progress, isLoading, error } = useUpload();
const handleFileChange = (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (file) {
upload({
file,
path: 'uploads/documents',
onSuccess: (result) => {
console.log('File uploaded:', result.url);
}
});
}
};
</script>
<template>
<div>
<input
type="file"
@change="handleFileChange"
:disabled="isLoading"
/>
<p v-if="isLoading">Uploading: {{ progress?.percentage }}%</p>
<p v-if="error">Error: {{ error.message }}</p>
</div>
</template>
Execute Feature
<script setup lang="ts">
import { useFeatureExecute } from '@ductape/vue';
const props = defineProps<{
orderId: string;
}>();
const { mutate, isLoading, data, error } = useFeatureExecute();
const handleProcess = () => {
mutate({
feature: 'process-order',
input: { orderId: props.orderId }
});
};
</script>
<template>
<div>
<button @click="handleProcess" :disabled="isLoading">
{{ isLoading ? 'Processing...' : 'Process Order' }}
</button>
<p v-if="data">Execution ID: {{ data.executionId }}</p>
<p v-if="error">Error: {{ error.message }}</p>
</div>
</template>
Accessing the Client Directly
You can access the underlying Ductape client instance:
<script setup lang="ts">
import { useDuctape } from '@ductape/vue';
const { client, isReady, isConnected } = useDuctape();
// Use client directly for operations not covered by composables
const handleCustomOperation = async () => {
const result = await client.databases.query({
table: 'custom_table'
});
console.log(result);
};
</script>
<template>
<div>
<p>Ready: {{ isReady ? 'Yes' : 'No' }}</p>
<p>Connected: {{ isConnected ? 'Yes' : 'No' }}</p>
<button @click="handleCustomOperation">
Run Custom Operation
</button>
</div>
</template>
Reactive Queries with Computed
Use Vue's reactivity system with Ductape composables:
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useDatabaseQuery } from '@ductape/vue';
const category = ref('electronics');
const priceLimit = ref(1000);
const queryKey = computed(() => ['products', category.value, priceLimit.value]);
const queryOptions = computed(() => ({
table: 'products',
where: {
category: category.value,
price: { $lt: priceLimit.value }
}
}));
const { data, isLoading } = useDatabaseQuery(queryKey, queryOptions);
</script>
<template>
<div>
<select v-model="category">
<option value="electronics">Electronics</option>
<option value="books">Books</option>
</select>
<input v-model.number="priceLimit" type="number" placeholder="Max price" />
<div v-if="isLoading">Loading...</div>
<div v-else>
<div v-for="product in data?.rows" :key="product.id">
{{ product.name }} - ${{ product.price }}
</div>
</div>
</div>
</template>
TypeScript Support
The composables are fully typed. You can provide type parameters:
<script setup lang="ts">
import { useDatabaseQuery } from '@ductape/vue';
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
const { data, isLoading } = useDatabaseQuery<User>(
['users'],
{
table: 'users',
limit: 10
}
);
// data.value?.rows is typed as User[]
</script>
<template>
<ul>
<li v-for="user in data?.rows" :key="user.id">
{{ user.name }} - {{ user.role }}
</li>
</ul>
</template>
Using Without the Plugin
You can use the composables without the plugin by creating a client manually:
<script setup lang="ts">
import { Ductape } from '@ductape/client';
import { useDatabaseQuery } from '@ductape/vue';
import { provide } from 'vue';
import { DUCTAPE_INJECTION_KEY } from '@ductape/vue';
// Create client
const client = new Ductape({
publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
});
// Provide to children
provide(DUCTAPE_INJECTION_KEY, client);
// Use composables
const { data } = useDatabaseQuery(['users'], {
table: 'users'
});
</script>
Best Practices
-
Use query keys wisely: The first parameter to composables like
useDatabaseQueryis a query key used for caching. Make it unique and reactive. -
Use computed for reactive queries: When query options depend on reactive state, use
computed:
<script setup lang="ts">
const userId = ref('123');
const queryOptions = computed(() => ({
table: 'posts',
where: { userId: userId.value }
}));
const { data } = useDatabaseQuery(['posts', userId], queryOptions);
</script>
-
Handle loading and error states: Always provide UI feedback in templates.
-
Cleanup is automatic: Subscriptions and watchers automatically cleanup when components unmount.
-
Use TypeScript: Leverage TypeScript with Vue 3 for type safety and better developer experience.
Composition API vs Options API
While we recommend the Composition API (<script setup>), you can also use the Options API:
<script lang="ts">
import { defineComponent } from 'vue';
import { useDatabaseQuery } from '@ductape/vue';
export default defineComponent({
setup() {
const { data, isLoading, error } = useDatabaseQuery(
['users'],
{ table: 'users', limit: 10 }
);
return {
data,
isLoading,
error
};
}
});
</script>
<template>
<div v-if="isLoading">Loading...</div>
<ul v-else>
<li v-for="user in data?.rows" :key="user.id">
{{ user.name }}
</li>
</ul>
</template>