Tutorial: Building with Flowfull Client ​
This comprehensive tutorial will guide you through building a complete application using @pubflow/flowfull-client, from basic setup to advanced features.
What We'll Build ​
We'll build a Product Management System with:
- User authentication
- Product listing with search and filters
- Product creation and updates
- Pagination and sorting
- Error handling
Prerequisites ​
- Node.js 16+ or Bun
- Basic knowledge of JavaScript/TypeScript
- A Flowfull backend (or use our demo API)
Step 1: Installation ​
First, create a new project and install the package:
# Create project
mkdir product-manager
cd product-manager
npm init -y
# Install dependencies
npm install @pubflow/flowfull-client
# For TypeScript (optional)
npm install -D typescript @types/nodeStep 2: Basic Setup ​
Create a file src/api.ts to configure the client:
// src/api.ts
import { createFlowfull } from '@pubflow/flowfull-client';
// Create API client
export const api = createFlowfull('https://api.myapp.com', {
// Optional: Add custom headers
headers: {
'X-Client-Version': '1.0.0'
},
// Optional: Configure retry
retryAttempts: 3,
// Optional: Set timeout
timeout: 30000,
// Optional: Add request logging
onRequest: async (config) => {
console.log(`[${config.method}] ${config.url}`);
},
onError: (error) => {
console.error('API Error:', error.message);
}
});Step 3: Authentication ​
Create authentication functions:
// src/auth.ts
import { api } from './api';
/**
* Login user
*/
export async function login(email: string, password: string) {
// Login endpoint doesn't need session
const response = await api.post('/auth/login',
{ email, password },
{ includeSession: false }
);
if (response.success) {
// Store session ID
api.setSessionId(response.data.session_id);
// Optionally store in localStorage for persistence
if (typeof window !== 'undefined') {
localStorage.setItem('pubflow_session_id', response.data.session_id);
}
return {
success: true,
user: response.data.user
};
}
return {
success: false,
error: response.error
};
}
/**
* Logout user
*/
export async function logout() {
// Call logout endpoint
await api.post('/auth/logout');
// Clear session
api.clearSession();
// Clear from localStorage
if (typeof window !== 'undefined') {
localStorage.removeItem('pubflow_session_id');
}
}
/**
* Get current user profile
*/
export async function getProfile() {
const response = await api.get('/profile');
if (response.success) {
return response.data;
}
return null;
}
/**
* Check if user is authenticated
*/
export async function isAuthenticated() {
return await api.hasSession();
}Step 4: Product Types ​
Define TypeScript types for type safety:
// src/types.ts
export interface Product {
id: string;
name: string;
description: string;
price: number;
category: string;
status: 'active' | 'inactive' | 'draft';
stock: number;
image_url?: string;
rating?: number;
created_at: string;
updated_at: string;
}
export interface ProductFilters {
search?: string;
category?: string;
minPrice?: number;
maxPrice?: number;
status?: string;
inStock?: boolean;
}
export interface PaginationParams {
page: number;
limit: number;
}
export interface ProductListResponse {
products: Product[];
total: number;
page: number;
totalPages: number;
}Step 5: Product Service ​
Create a service to handle all product operations:
// src/products.ts
import { api } from './api';
import { between, gte, lte, inOp, isNotNull } from '@pubflow/flowfull-client';
import type { Product, ProductFilters, PaginationParams } from './types';
/**
* Get all products with filters and pagination
*/
export async function getProducts(
filters: ProductFilters = {},
pagination: PaginationParams = { page: 1, limit: 20 }
) {
// Start building query
let query = api.query('/products');
// Add search if provided
if (filters.search) {
query = query.search(filters.search);
}
// Add category filter
if (filters.category) {
query = query.where('category', filters.category);
}
// Add price range filter
if (filters.minPrice !== undefined && filters.maxPrice !== undefined) {
query = query.where('price', between(filters.minPrice, filters.maxPrice));
} else if (filters.minPrice !== undefined) {
query = query.where('price', gte(filters.minPrice));
} else if (filters.maxPrice !== undefined) {
query = query.where('price', lte(filters.maxPrice));
}
// Add status filter
if (filters.status) {
query = query.where('status', filters.status);
}
// Add stock filter
if (filters.inStock) {
query = query.where('stock', gte(1));
}
// Add pagination
query = query.page(pagination.page).limit(pagination.limit);
// Add sorting (default: newest first)
query = query.sort('created_at', 'desc');
// Execute query
const response = await query.get<Product[]>();
if (response.success) {
return {
success: true,
products: response.data,
meta: response.meta
};
}
return {
success: false,
error: response.error
};
}
/**
* Get a single product by ID
*/
export async function getProduct(id: string) {
const response = await api.get<Product>(`/products/${id}`);
if (response.success) {
return {
success: true,
product: response.data
};
}
return {
success: false,
error: response.error
};
}
/**
* Create a new product
*/
export async function createProduct(data: Omit<Product, 'id' | 'created_at' | 'updated_at'>) {
const response = await api.post<Product>('/products', data);
if (response.success) {
return {
success: true,
product: response.data
};
}
return {
success: false,
error: response.error
};
}
/**
* Update a product
*/
export async function updateProduct(id: string, data: Partial<Product>) {
const response = await api.patch<Product>(`/products/${id}`, data);
if (response.success) {
return {
success: true,
product: response.data
};
}
return {
success: false,
error: response.error
};
}
/**
* Delete a product
*/
export async function deleteProduct(id: string) {
const response = await api.delete(`/products/${id}`);
return {
success: response.success,
error: response.error
};
}
/**
* Get products by category
*/
export async function getProductsByCategory(category: string, page = 1, limit = 20) {
const response = await api
.query('/products')
.where('category', category)
.where('status', 'active')
.sort('name', 'asc')
.page(page)
.limit(limit)
.get<Product[]>();
if (response.success) {
return {
success: true,
products: response.data,
meta: response.meta
};
}
return {
success: false,
error: response.error
};
}
/**
* Search products
*/
export async function searchProducts(searchTerm: string, page = 1, limit = 20) {
const response = await api
.query('/products')
.search(searchTerm)
.where('status', 'active')
.sort('rating', 'desc')
.page(page)
.limit(limit)
.get<Product[]>();
if (response.success) {
return {
success: true,
products: response.data,
meta: response.meta
};
}
return {
success: false,
error: response.error
};
}
/**
* Get featured products
*/
export async function getFeaturedProducts(limit = 10) {
const response = await api
.query('/products')
.where('status', 'active')
.where('rating', gte(4.5))
.where('image_url', isNotNull())
.sort('rating', 'desc')
.limit(limit)
.get<Product[]>();
if (response.success) {
return {
success: true,
products: response.data
};
}
return {
success: false,
error: response.error
};
}Step 6: Using the Product Service ​
Now let's use our product service in a real application:
// src/main.ts
import { login, logout, getProfile, isAuthenticated } from './auth';
import {
getProducts,
getProduct,
createProduct,
updateProduct,
deleteProduct,
searchProducts,
getFeaturedProducts
} from './products';
async function main() {
console.log('=== Product Management System ===\n');
// Step 1: Login
console.log('1. Logging in...');
const loginResult = await login('user@example.com', 'password123');
if (!loginResult.success) {
console.error('Login failed:', loginResult.error);
return;
}
console.log('✅ Logged in as:', loginResult.user.name);
console.log('');
// Step 2: Get featured products
console.log('2. Getting featured products...');
const featuredResult = await getFeaturedProducts(5);
if (featuredResult.success) {
console.log(`✅ Found ${featuredResult.products.length} featured products:`);
featuredResult.products.forEach(product => {
console.log(` - ${product.name} ($${product.price}) - Rating: ${product.rating}`);
});
}
console.log('');
// Step 3: Search products
console.log('3. Searching for "laptop"...');
const searchResult = await searchProducts('laptop', 1, 10);
if (searchResult.success) {
console.log(`✅ Found ${searchResult.meta?.total} products`);
console.log(` Showing page ${searchResult.meta?.page} of ${searchResult.meta?.totalPages}`);
searchResult.products.forEach(product => {
console.log(` - ${product.name} ($${product.price})`);
});
}
console.log('');
// Step 4: Get products with filters
console.log('4. Getting electronics under $1000...');
const filteredResult = await getProducts({
category: 'electronics',
maxPrice: 1000,
status: 'active',
inStock: true
}, {
page: 1,
limit: 10
});
if (filteredResult.success) {
console.log(`✅ Found ${filteredResult.meta?.total} products`);
filteredResult.products.forEach(product => {
console.log(` - ${product.name} ($${product.price}) - Stock: ${product.stock}`);
});
}
console.log('');
// Step 5: Create a new product
console.log('5. Creating a new product...');
const newProductResult = await createProduct({
name: 'Gaming Laptop Pro',
description: 'High-performance gaming laptop',
price: 1499.99,
category: 'electronics',
status: 'active',
stock: 10,
rating: 4.8
});
if (newProductResult.success) {
console.log('✅ Product created:', newProductResult.product.id);
// Step 6: Update the product
console.log('6. Updating product...');
const updateResult = await updateProduct(newProductResult.product.id, {
price: 1399.99,
stock: 15
});
if (updateResult.success) {
console.log('✅ Product updated');
console.log(` New price: $${updateResult.product.price}`);
console.log(` New stock: ${updateResult.product.stock}`);
}
// Step 7: Get the updated product
console.log('7. Getting updated product...');
const getResult = await getProduct(newProductResult.product.id);
if (getResult.success) {
console.log('✅ Product details:');
console.log(` Name: ${getResult.product.name}`);
console.log(` Price: $${getResult.product.price}`);
console.log(` Stock: ${getResult.product.stock}`);
console.log(` Status: ${getResult.product.status}`);
}
// Step 8: Delete the product
console.log('8. Deleting product...');
const deleteResult = await deleteProduct(newProductResult.product.id);
if (deleteResult.success) {
console.log('✅ Product deleted');
}
}
console.log('');
// Step 9: Logout
console.log('9. Logging out...');
await logout();
console.log('✅ Logged out');
// Step 10: Verify logged out
const isAuth = await isAuthenticated();
console.log(` Authenticated: ${isAuth}`);
}
// Run the application
main().catch(console.error);Step 7: Advanced Filtering ​
Let's create more advanced filtering examples:
// src/advanced-filters.ts
import { api } from './api';
import {
between,
gte,
lte,
inOp,
notIn,
isNotNull,
isNull,
startsWith,
endsWith,
ilike
} from '@pubflow/flowfull-client';
/**
* Get premium products (high price, high rating, in stock)
*/
export async function getPremiumProducts() {
return await api
.query('/products')
.where('price', gte(1000))
.where('rating', gte(4.5))
.where('stock', gte(1))
.where('status', 'active')
.where('image_url', isNotNull())
.sort('rating', 'desc')
.limit(20)
.get();
}
/**
* Get budget products (low price, in stock)
*/
export async function getBudgetProducts() {
return await api
.query('/products')
.where('price', lte(100))
.where('stock', gte(1))
.where('status', 'active')
.sort('price', 'asc')
.limit(20)
.get();
}
/**
* Get products in specific price range
*/
export async function getProductsInPriceRange(min: number, max: number) {
return await api
.query('/products')
.where('price', between(min, max))
.where('status', 'active')
.sort('price', 'asc')
.get();
}
/**
* Get products by multiple categories
*/
export async function getProductsByCategories(categories: string[]) {
return await api
.query('/products')
.where('category', inOp(categories))
.where('status', 'active')
.sort('name', 'asc')
.get();
}
/**
* Get products excluding certain categories
*/
export async function getProductsExcludingCategories(categories: string[]) {
return await api
.query('/products')
.where('category', notIn(categories))
.where('status', 'active')
.get();
}
/**
* Get products with images
*/
export async function getProductsWithImages() {
return await api
.query('/products')
.where('image_url', isNotNull())
.where('status', 'active')
.get();
}
/**
* Get products without images (need images)
*/
export async function getProductsNeedingImages() {
return await api
.query('/products')
.where('image_url', isNull())
.where('status', 'active')
.get();
}
/**
* Get products by name pattern
*/
export async function getProductsByNamePattern(pattern: string) {
return await api
.query('/products')
.where('name', ilike(pattern)) // Case-insensitive
.where('status', 'active')
.get();
}
/**
* Get products starting with specific text
*/
export async function getProductsStartingWith(text: string) {
return await api
.query('/products')
.where('name', startsWith(text))
.where('status', 'active')
.get();
}
/**
* Complex filter: Gaming products in stock, mid-range price
*/
export async function getGamingProducts() {
return await api
.query('/products')
.search('gaming')
.where('category', inOp(['electronics', 'computers', 'gaming']))
.where('price', between(500, 2000))
.where('stock', gte(1))
.where('rating', gte(4.0))
.where('status', 'active')
.sort('rating', 'desc')
.page(1)
.limit(20)
.get();
}Step 8: Error Handling ​
Implement robust error handling:
// src/error-handler.ts
import type { ApiResponse } from '@pubflow/flowfull-client';
export class ApiError extends Error {
constructor(
message: string,
public status: number,
public code?: string
) {
super(message);
this.name = 'ApiError';
}
}
/**
* Handle API response with proper error handling
*/
export function handleResponse<T>(response: ApiResponse<T>) {
if (response.success) {
return response.data;
}
// Handle specific error codes
switch (response.status) {
case 400:
throw new ApiError('Bad request: ' + response.error, 400);
case 401:
throw new ApiError('Unauthorized - please login', 401);
case 403:
throw new ApiError('Forbidden - insufficient permissions', 403);
case 404:
throw new ApiError('Resource not found', 404);
case 422:
throw new ApiError('Validation error: ' + response.error, 422);
case 429:
throw new ApiError('Too many requests - please try again later', 429);
case 500:
throw new ApiError('Server error - please try again', 500);
default:
throw new ApiError(response.error || 'Unknown error', response.status);
}
}
/**
* Retry function with exponential backoff
*/
export async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
// Exponential backoff
const delay = baseDelay * Math.pow(2, i);
console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('Max retries exceeded');
}
/**
* Example usage with error handling
*/
export async function getProductsSafely() {
try {
const response = await api.get('/products');
return handleResponse(response);
} catch (error) {
if (error instanceof ApiError) {
console.error(`API Error [${error.status}]:`, error.message);
// Handle specific errors
if (error.status === 401) {
// Redirect to login
console.log('Redirecting to login...');
}
} else {
console.error('Unexpected error:', error);
}
throw error;
}
}Step 9: React Integration ​
Use Flowfull Client in a React application:
// src/hooks/useProducts.ts
import { useState, useEffect } from 'react';
import { getProducts } from '../products';
import type { Product, ProductFilters, PaginationParams } from '../types';
export function useProducts(
filters: ProductFilters = {},
pagination: PaginationParams = { page: 1, limit: 20 }
) {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [meta, setMeta] = useState<any>(null);
useEffect(() => {
const fetchProducts = async () => {
setLoading(true);
setError(null);
const result = await getProducts(filters, pagination);
if (result.success) {
setProducts(result.products);
setMeta(result.meta);
} else {
setError(result.error || 'Failed to fetch products');
}
setLoading(false);
};
fetchProducts();
}, [JSON.stringify(filters), pagination.page, pagination.limit]);
return { products, loading, error, meta };
}
// src/components/ProductList.tsx
import React from 'react';
import { useProducts } from '../hooks/useProducts';
export function ProductList() {
const { products, loading, error, meta } = useProducts(
{ status: 'active', inStock: true },
{ page: 1, limit: 20 }
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>Products</h1>
<p>Total: {meta?.total} products</p>
<div className="product-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>{product.description}</p>
<p className="price">${product.price}</p>
<p className="stock">Stock: {product.stock}</p>
</div>
))}
</div>
{meta && (
<div className="pagination">
<p>Page {meta.page} of {meta.totalPages}</p>
</div>
)}
</div>
);
}Step 10: React Native Integration ​
Use in a React Native/Expo app:
// src/screens/ProductsScreen.tsx
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, ActivityIndicator } from 'react-native';
import { getProducts } from '../products';
import type { Product } from '../types';
export function ProductsScreen() {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const loadProducts = async () => {
const result = await getProducts(
{ status: 'active' },
{ page: 1, limit: 20 }
);
if (result.success) {
setProducts(result.products);
}
setLoading(false);
setRefreshing(false);
};
useEffect(() => {
loadProducts();
}, []);
const handleRefresh = () => {
setRefreshing(true);
loadProducts();
};
if (loading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</View>
);
}
return (
<FlatList
data={products}
keyExtractor={item => item.id}
refreshing={refreshing}
onRefresh={handleRefresh}
renderItem={({ item }) => (
<View style={{ padding: 16, borderBottomWidth: 1 }}>
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>{item.name}</Text>
<Text>{item.description}</Text>
<Text style={{ fontSize: 16, color: 'green' }}>${item.price}</Text>
<Text>Stock: {item.stock}</Text>
</View>
)}
/>
);
}Summary ​
Congratulations! You've learned how to:
✅ Install and configure Flowfull Client ✅ Implement authentication (login/logout) ✅ Create a complete product service ✅ Use the query builder with filters ✅ Handle pagination and sorting ✅ Implement error handling ✅ Integrate with React and React Native ✅ Use all 14 filter operators ✅ Build complex queries
Next Steps ​
- API Reference - Complete API documentation
- Examples - More real-world examples
- Advanced Topics - Custom storage, multi-instance, offline support
Complete Example Repository ​
Check out the complete example on GitHub: