Skip to content

Data Fetching

Learn how to execute queries, handle responses, and manage data retrieval patterns with PGRestify.

Overview

Data fetching in PGRestify involves executing queries built with the query builder and handling the responses. The library provides multiple execution methods, response handling patterns, and strategies for different data access scenarios.

Basic Query Execution

The execute() Method

The execute() method is the primary way to run queries and retrieve data:

typescript
import { createClient } from '@webcoded/pgrestify';

const client = createClient({
  url: 'http://localhost:3000'
});

// Basic execution
const result = await client
  .from('users')
  .select('*')
  .execute();

console.log(result.data);    // Array of user objects
console.log(result.count);   // Total count (if requested)
console.log(result.status);  // HTTP status code
console.log(result.error);   // Error object (if any)

Response Structure

All query executions return a standardized response object:

typescript
interface QueryResponse<T> {
  data: T[];           // The actual data returned
  count?: number;      // Total count (when count option is used)
  status: number;      // HTTP status code
  statusText: string;  // HTTP status message
  error?: any;         // Error object if something went wrong
}

// Example usage
const response = await client
  .from('posts')
  .select('*')
  .execute();

if (response.error) {
  console.error('Query failed:', response.error);
} else {
  console.log(`Retrieved ${response.data.length} posts`);
  response.data.forEach(post => {
    console.log(`- ${post.title}`);
  });
}

Single Record Fetching

single() Method

When you expect exactly one record, use the single() method:

typescript
// Get a specific user by ID
const userResponse = await client
  .from('users')
  .select('*')
  .eq('id', 123)
  .single()
  .execute();

if (userResponse.data) {
  console.log('User found:', userResponse.data.name);
} else {
  console.log('User not found');
}

maybeSingle() Method

When you expect zero or one record:

typescript
// Find user by email (might not exist)
const userResponse = await client
  .from('users')
  .select('*')
  .eq('email', 'user@example.com')
  .maybeSingle()
  .execute();

if (userResponse.data) {
  console.log('User exists:', userResponse.data.name);
} else {
  console.log('No user with that email');
}

Error Handling for Single Records

typescript
import { PGRestifyError } from '@webcoded/pgrestify';

try {
  const user = await client
    .from('users')
    .select('*')
    .eq('id', 123)
    .single()
    .execute();
    
  console.log('User:', user.data);
} catch (error) {
  if (error instanceof PGRestifyError) {
    if (error.code === 'PGRST116') {
      console.log('No user found with that ID');
    } else if (error.code === 'PGRST117') {
      console.log('Multiple users found (expected one)');
    } else {
      console.error('Database error:', error.message);
    }
  }
}

Counting Records

Getting Counts with Data

typescript
// Get data and exact count
const response = await client
  .from('posts')
  .select('*', { count: 'exact' })
  .eq('published', true)
  .execute();

console.log(`Showing ${response.data.length} of ${response.count} published posts`);

Count-Only Queries

typescript
// Get only the count (no data)
const countResponse = await client
  .from('users')
  .select('*', { count: 'exact', head: true })
  .eq('active', true)
  .execute();

console.log(`Active users: ${countResponse.count}`);

Estimated Counts

For better performance on large tables, use estimated counts:

typescript
// Fast estimated count
const estimatedCount = await client
  .from('large_table')
  .select('*', { count: 'estimated', head: true })
  .execute();

console.log(`Approximately ${estimatedCount.count} records`);

Async/Await Patterns

Sequential Fetching

typescript
const fetchUserData = async (userId: number) => {
  // Fetch user
  const user = await client
    .from('users')
    .select('*')
    .eq('id', userId)
    .single()
    .execute();

  // Fetch user's posts
  const posts = await client
    .from('posts')
    .select('*')
    .eq('user_id', userId)
    .order('created_at', { ascending: false })
    .execute();

  // Fetch user's profile
  const profile = await client
    .from('user_profiles')
    .select('*')
    .eq('user_id', userId)
    .maybeSingle()
    .execute();

  return {
    user: user.data,
    posts: posts.data,
    profile: profile.data
  };
};

Parallel Fetching

typescript
const fetchDashboardData = async (userId: number) => {
  // Execute multiple queries in parallel
  const [userResponse, postsResponse, profileResponse] = await Promise.all([
    client
      .from('users')
      .select('*')
      .eq('id', userId)
      .single()
      .execute(),

    client
      .from('posts')
      .select('*')
      .eq('user_id', userId)
      .order('created_at', { ascending: false })
      .limit(10)
      .execute(),

    client
      .from('user_profiles')
      .select('*')
      .eq('user_id', userId)
      .maybeSingle()
      .execute()
  ]);

  return {
    user: userResponse.data,
    posts: postsResponse.data,
    profile: profileResponse.data
  };
};

Error Handling Strategies

Comprehensive Error Handling

typescript
import { PGRestifyError } from '@webcoded/pgrestify';

const fetchWithErrorHandling = async (table: string, filters: any) => {
  try {
    const response = await client
      .from(table)
      .select('*')
      .match(filters)
      .execute();

    return {
      success: true,
      data: response.data,
      count: response.count
    };

  } catch (error) {
    if (error instanceof PGRestifyError) {
      // Handle PostgREST-specific errors
      return {
        success: false,
        error: {
          type: 'database',
          code: error.code,
          message: error.message,
          details: error.details
        }
      };
    } else {
      // Handle network or other errors
      return {
        success: false,
        error: {
          type: 'network',
          message: error.message
        }
      };
    }
  }
};

// Usage
const result = await fetchWithErrorHandling('users', { active: true });
if (result.success) {
  console.log('Users:', result.data);
} else {
  console.error('Error:', result.error);
}

Retry Logic

typescript
const fetchWithRetry = async (
  queryFn: () => Promise<any>,
  maxRetries = 3,
  delay = 1000
) => {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await queryFn();
    } catch (error) {
      if (attempt === maxRetries) {
        throw error;
      }
      
      // Wait before retrying
      await new Promise(resolve => setTimeout(resolve, delay * attempt));
      console.log(`Retry attempt ${attempt} failed, trying again...`);
    }
  }
};

// Usage
const fetchUsersWithRetry = () => fetchWithRetry(
  () => client.from('users').select('*').execute(),
  3,
  1000
);

Data Transformation

Column Transformation

PGRestify can automatically transform column names between snake_case and camelCase:

typescript
// Enable column transformation
const client = createClient({
  url: 'http://localhost:3000',
  transformColumns: true
});

// Database has: user_id, first_name, last_name, created_at
const users = await client
  .from('users')
  .select('*')
  .execute();

// Result has: userId, firstName, lastName, createdAt
console.log(users.data[0].firstName); // Instead of first_name

Custom Data Processing

typescript
const fetchAndProcessUsers = async () => {
  const response = await client
    .from('users')
    .select('id, name, email, created_at, active')
    .execute();

  // Process the raw data
  const processedUsers = response.data.map(user => ({
    ...user,
    displayName: user.name || 'Anonymous',
    accountAge: calculateAge(user.created_at),
    status: user.active ? 'Active' : 'Inactive'
  }));

  return {
    users: processedUsers,
    totalCount: response.count,
    activeCount: processedUsers.filter(u => u.active).length
  };
};

const calculateAge = (createdAt: string): number => {
  const now = new Date();
  const created = new Date(createdAt);
  return Math.floor((now.getTime() - created.getTime()) / (1000 * 60 * 60 * 24));
};

Streaming and Large Datasets

Chunked Processing

typescript
const processLargeDataset = async (chunkSize = 1000) => {
  let offset = 0;
  let hasMore = true;

  while (hasMore) {
    const chunk = await client
      .from('large_table')
      .select('*')
      .range(offset, offset + chunkSize - 1)
      .execute();

    // Process chunk
    await processChunk(chunk.data);

    // Check if there are more records
    hasMore = chunk.data.length === chunkSize;
    offset += chunkSize;

    console.log(`Processed ${Math.min(offset, offset - chunkSize + chunk.data.length)} records`);
  }
};

const processChunk = async (data: any[]) => {
  // Process each chunk of data
  for (const record of data) {
    // Perform processing logic
    await processRecord(record);
  }
};

Cursor-Based Streaming

typescript
const streamData = async (
  processor: (record: any) => Promise<void>,
  batchSize = 100
) => {
  let lastId = 0;
  let hasMore = true;

  while (hasMore) {
    const batch = await client
      .from('events')
      .select('*')
      .gt('id', lastId)
      .order('id')
      .limit(batchSize)
      .execute();

    // Process each record in the batch
    for (const record of batch.data) {
      await processor(record);
      lastId = record.id;
    }

    hasMore = batch.data.length === batchSize;
  }
};

// Usage
await streamData(async (event) => {
  console.log('Processing event:', event.id);
  // Perform processing logic
});

Caching Strategies

Built-in Query Caching

typescript
// Enable client-side caching
const client = createClient({
  url: 'http://localhost:3000',
  cache: {
    ttl: 300000, // 5 minutes
    max: 100     // Maximum 100 cached queries
  }
});

// Cached queries (automatically cached for 5 minutes)
const users = await client
  .from('users')
  .select('*')
  .eq('active', true)
  .execute(); // First call - hits database

const usersAgain = await client
  .from('users')
  .select('*')
  .eq('active', true)
  .execute(); // Second call - returns cached result

Manual Cache Control

typescript
// Clear specific cache entries
client.cache.invalidate('users', { active: true });

// Clear all cache
client.cache.clear();

// Check if query is cached
const isCached = client.cache.has('users', { active: true });

// Get cached value without executing query
const cachedResult = client.cache.get('users', { active: true });

Cache-First Pattern

typescript
const fetchWithCacheFirst = async (key: string, queryFn: () => Promise<any>) => {
  // Try cache first
  const cached = client.cache.get(key);
  if (cached) {
    return { data: cached, fromCache: true };
  }

  // Execute query and cache result
  const result = await queryFn();
  client.cache.set(key, result.data, 300000); // Cache for 5 minutes
  
  return { data: result.data, fromCache: false };
};

Response Handling Patterns

Generic Response Handler

typescript
interface ApiResponse<T> {
  data?: T;
  error?: string;
  loading?: boolean;
}

const handleApiResponse = async <T>(
  queryPromise: Promise<any>
): Promise<ApiResponse<T>> => {
  try {
    const response = await queryPromise;
    
    if (response.error) {
      return { error: response.error.message };
    }
    
    return { data: response.data };
  } catch (error) {
    return { 
      error: error instanceof Error ? error.message : 'Unknown error' 
    };
  }
};

// Usage
const userResponse = await handleApiResponse(
  client
    .from('users')
    .select('*')
    .eq('id', 123)
    .single()
    .execute()
);

if (userResponse.error) {
  console.error('Error:', userResponse.error);
} else if (userResponse.data) {
  console.log('User:', userResponse.data);
}

Typed Response Handlers

typescript
interface User {
  id: number;
  name: string;
  email: string;
  created_at: string;
}

const fetchTypedUser = async (id: number): Promise<User | null> => {
  try {
    const response = await client
      .from<User>('users')
      .select('*')
      .eq('id', id)
      .single()
      .execute();
      
    return response.data;
  } catch (error) {
    if (error instanceof PGRestifyError && error.code === 'PGRST116') {
      return null; // User not found
    }
    throw error; // Re-throw other errors
  }
};

Data Fetching Best Practices

Selective Field Loading

typescript
// Good: Load only needed fields
const loadUserSummary = async () => {
  return client
    .from('users')
    .select('id, name, email, last_login')
    .eq('active', true)
    .execute();
};

// Avoid: Loading unnecessary data
const inefficientLoad = async () => {
  return client
    .from('users')
    .select('*') // Loads all columns including large text fields
    .eq('active', true)
    .execute();
};

Efficient Relationship Loading

typescript
// Good: Load related data in single query
const loadPostsWithAuthor = async () => {
  return client
    .from('posts')
    .select(`
      id,
      title,
      content,
      created_at,
      author:users (
        id,
        name,
        avatar_url
      )
    `)
    .eq('published', true)
    .execute();
};

// Avoid: N+1 queries
const inefficientLoad = async () => {
  const posts = await client
    .from('posts')
    .select('*')
    .eq('published', true)
    .execute();

  // This creates N additional queries!
  for (const post of posts.data) {
    const author = await client
      .from('users')
      .select('*')
      .eq('id', post.user_id)
      .single()
      .execute();
      
    post.author = author.data;
  }
  
  return posts.data;
};

Pagination Strategies

typescript
// Efficient pagination for UI
const paginateResults = async (page: number, pageSize: number) => {
  const start = page * pageSize;
  const end = start + pageSize - 1;

  return client
    .from('posts')
    .select('*', { count: 'exact' })
    .range(start, end)
    .order('created_at', { ascending: false })
    .execute();
};

// Cursor-based pagination for performance
const cursorPaginate = async (cursor?: string, limit = 20) => {
  let query = client
    .from('posts')
    .select('*')
    .order('created_at', { ascending: false })
    .limit(limit);

  if (cursor) {
    query = query.lt('created_at', cursor);
  }

  return query.execute();
};

Conditional Fetching

typescript
const fetchUserData = async (userId: number, includeProfile = false) => {
  let select = 'id, name, email, created_at';
  
  if (includeProfile) {
    select += ', profile:user_profiles(*)';
  }

  return client
    .from('users')
    .select(select)
    .eq('id', userId)
    .single()
    .execute();
};

Performance Monitoring

Query Performance Tracking

typescript
const performanceWrapper = async <T>(
  queryName: string,
  queryFn: () => Promise<T>
): Promise<T> => {
  const startTime = performance.now();
  
  try {
    const result = await queryFn();
    const endTime = performance.now();
    
    console.log(`Query "${queryName}" took ${endTime - startTime}ms`);
    return result;
  } catch (error) {
    const endTime = performance.now();
    console.error(`Query "${queryName}" failed after ${endTime - startTime}ms:`, error);
    throw error;
  }
};

// Usage
const users = await performanceWrapper(
  'load-active-users',
  () => client
    .from('users')
    .select('*')
    .eq('active', true)
    .execute()
);

Memory Management

typescript
// Process large datasets without loading everything into memory
const processLargeTable = async () => {
  const batchSize = 1000;
  let offset = 0;
  let processedCount = 0;

  while (true) {
    const batch = await client
      .from('large_table')
      .select('*')
      .range(offset, offset + batchSize - 1)
      .execute();

    if (batch.data.length === 0) break;

    // Process batch and immediately release memory
    await processBatch(batch.data);
    processedCount += batch.data.length;
    
    // Clear processed data from memory
    batch.data.length = 0;
    
    offset += batchSize;
    
    console.log(`Processed ${processedCount} records`);
  }
};

Summary

Effective data fetching with PGRestify involves:

  • Query Execution: Using execute(), single(), and maybeSingle() appropriately
  • Response Handling: Understanding the response structure and error patterns
  • Performance: Optimizing queries, using efficient pagination, and avoiding N+1 problems
  • Error Management: Implementing comprehensive error handling and retry logic
  • Caching: Leveraging built-in caching for better performance
  • Type Safety: Using TypeScript interfaces for better development experience
  • Memory Management: Processing large datasets efficiently

The key is to match your data fetching strategy to your application's specific needs while maintaining good performance and user experience.

Released under the MIT License.