The Query Builder is the heart of Flowfull Client. It provides a chainable, fluent API for building complex queries with ease.
Instead of manually constructing query strings, you chain methods together:
// ❌ Manual way (error-prone)
const url = '/products?status=active&price_gte=50&page=1&limit=20&sort=price&order=asc';
// ✅ Query Builder way (clean and type-safe)
const response = await api
.query('/products')
.where('status', 'active')
.where('price', 'gte', 50)
.page(1)
.limit(20)
.sort('price', 'asc')
.get();Start a query with the query() method:
const builder = api.query('/endpoint');Use where() to add filters:
// Simple equality
api.query('/users')
.where('status', 'active')
.get();
// With operator
api.query('/users')
.where('age', 'gte', 18)
.get();
// With filter helper
import { gte } from '@pubflow/flowfull-client';
api.query('/users')
.where('age', gte(18))
.get();Chain multiple where() calls:
api.query('/products')
.where('status', 'active')
.where('price', 'gte', 50)
.where('price', 'lte', 200)
.where('stock', 'gt', 0)
.get();Use filter() as a shorthand for equality:
// These are equivalent
api.query('/users').where('status', 'active').get();
api.query('/users').filter('status', 'active').get();Add full-text search with search() or q():
// Search for products
api.query('/products')
.search('laptop')
.get();
// Shorthand
api.query('/products')
.q('laptop')
.get();
// Combine with filters
api.query('/products')
.search('laptop')
.where('price', 'lte', 1000)
.get();// Get page 2 with 25 items per page
api.query('/users')
.page(2)
.limit(25)
.get();
// Alternative: perPage() alias
api.query('/users')
.page(2)
.perPage(25)
.get();// Skip first 50 items, get next 25
api.query('/users')
.offset(50)
.limit(25)
.get();const response = await api
.query('/users')
.page(1)
.limit(20)
.get();
if (response.success && response.meta) {
console.log(`Page ${response.meta.page} of ${response.meta.totalPages}`);
console.log(`Total items: ${response.meta.total}`);
console.log(`Has more: ${response.meta.hasMore}`);
}// Sort by name ascending
api.query('/users')
.sort('name', 'asc')
.get();
// Sort by created_at descending
api.query('/users')
.sort('created_at', 'desc')
.get();
// Default direction is 'asc'
api.query('/users')
.sort('name')
.get();// Sort by status (asc), then by name (asc)
api.query('/users')
.sort('status', 'asc')
.sort('name', 'asc')
.get();// These are equivalent
api.query('/users').sort('name', 'asc').get();
api.query('/users').orderBy('name', 'asc').get();Add custom query parameters:
// Single parameter
api.query('/users')
.param('include', 'profile')
.get();
// Multiple parameters
api.query('/users')
.params({
include: 'profile,settings',
fields: 'id,name,email',
expand: 'organization'
})
.get();The .param() method automatically detects bracket syntax field[operator]=value and converts it to filters:
// ✅ Using bracket syntax (auto-detected as filters)
api.query('/products')
.param('category[in]', 'electronics,computers')
.param('price[gte]', 100)
.param('price[lte]', 1000)
.get();
// ✅ Equivalent using .where() with helper functions
import { inOp, gte, lte } from '@pubflow/flowfull-client';
api.query('/products')
.where('category', inOp(['electronics', 'computers']))
.where('price', gte(100))
.where('price', lte(1000))
.get();
// Both generate the same URL:
// /products?category[in]=electronics,computers&price[gte]=100&price[lte]=1000Supported bracket operators:
field[eq]=value- Equals (or justfield=value)field[ne]=value- Not equalsfield[gt]=value- Greater thanfield[gte]=value- Greater than or equalsfield[lt]=value- Less thanfield[lte]=value- Less than or equalsfield[like]=value- Contains (case-sensitive)field[ilike]=value- Contains (case-insensitive)field[contains]=value- Alias forlikefield[startsWith]=value- Starts withfield[endsWith]=value- Ends withfield[in]=value1,value2- In arrayfield[nin]=value1,value2- Not in arrayfield[null]=true- Is nullfield[notNull]=true- Is not nullfield[between]=min,max- Between two valuesfield[notBetween]=min,max- Not between two values
Why bracket syntax?
- ✅ Compatible with @pubflow/bridge backend
- ✅ Industry standard (Laravel, Rails, PostgREST)
- ✅ Clear separation between field names and operators
- ✅ Universal across all Flowfull clients (TypeScript, Go, Python, Dart, .NET)
See Universal Filter Syntax for more details.
Add custom headers to your request:
// Single header
api.query('/users')
.header('X-API-Version', 'v2')
.get();
// Multiple headers
api.query('/users')
.headers({
'X-API-Version': 'v2',
'X-Client-ID': 'web-app',
'X-Request-ID': 'abc123'
})
.get();const builder = api.query('/users')
.where('status', 'active')
.page(1)
.limit(20);
const queryString = builder.toQueryString();
// "status=active&page=1&limit=20"const url = builder.toURL();
// "/users?status=active&page=1&limit=20"const queryString = builder.build();
// Returns the query stringconst response = await api
.query('/users')
.where('status', 'active')
.get();const body = { name: 'John', email: 'john@example.com' };
const response = await api
.query('/users')
.post(body);const updates = { name: 'John Updated' };
const response = await api
.query('/users/123')
.put(updates);const response = await api
.query('/users/123')
.patch({ status: 'inactive' });const response = await api
.query('/users/123')
.delete();All methods return this, allowing you to chain them:
const response = await api
.query('/products')
.search('laptop')
.where('status', 'active')
.where('price', 'gte', 500)
.where('price', 'lte', 2000)
.where('stock', 'gt', 0)
.sort('price', 'asc')
.page(1)
.limit(20)
.param('include', 'images,reviews')
.header('X-API-Version', 'v2')
.get();const products = await api
.query('/products')
.search(searchTerm)
.where('category', selectedCategory)
.where('price', 'gte', minPrice)
.where('price', 'lte', maxPrice)
.where('in_stock', true)
.sort(sortBy, sortDirection)
.page(currentPage)
.limit(itemsPerPage)
.get();const users = await api
.query('/users')
.where('role', inOp(['admin', 'moderator']))
.where('status', 'active')
.where('verified_at', isNotNull())
.where('created_at', 'gte', startDate)
.where('created_at', 'lte', endDate)
.sort('created_at', 'desc')
.page(page)
.limit(50)
.param('include', 'profile,stats')
.get();const orders = await api
.query('/orders')
.where('user_id', userId)
.where('status', inOp(['completed', 'shipped', 'delivered']))
.where('total', 'gte', 100)
.sort('created_at', 'desc')
.limit(20)
.param('include', 'items,shipping')
.get();const stats = await api
.query('/analytics/events')
.where('event_type', 'purchase')
.where('created_at', between(startDate, endDate))
.where('amount', 'gte', 50)
.param('group_by', 'date')
.param('aggregate', 'sum,count,avg')
.get();- Chain everything: Every method returns
thisfor chaining - Type safety: TypeScript will autocomplete all methods
- Reusable builders: Save a builder and execute it multiple times
- Combine with operators: Import filter operators for complex queries
- Debug queries: Use
toURL()to see the final query string
You can save a query builder and execute it multiple times:
// Create a reusable query
const activeUsersQuery = api
.query('/users')
.where('status', 'active')
.where('verified_at', isNotNull());
// Execute with different pagination
const page1 = await activeUsersQuery.page(1).limit(20).get();
const page2 = await activeUsersQuery.page(2).limit(20).get();Next: Configuration Guide