Skip to content

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:

bash
# 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/node

Step 2: Basic Setup ​

Create a file src/api.ts to configure the client:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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 ​

Complete Example Repository ​

Check out the complete example on GitHub: