Skip to content

Basic Usage Examples ​

This guide shows you how to get started with PGRestify using practical examples with both PostgREST native syntax and repository patterns.

Setup ​

First, install PGRestify:

bash
npm install @webcoded/pgrestify

Define Your Types ​

typescript
// Define interfaces for type safety
interface User {
  id: number;
  name: string;
  email: string;
  active: boolean;
  role: 'user' | 'admin' | 'moderator';
  created_at: string;
  updated_at?: string;
}

interface Post {
  id: number;
  title: string;
  content: string;
  author_id: number;
  published: boolean;
  tags: string[];
  created_at: string;
}

Create a Client ​

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

// Basic client setup
const client = createClient({
  url: 'http://localhost:3000', // Your PostgREST URL
  auth: {
    persistSession: true
  }
});

// Advanced client with caching
const client = createClient({
  url: process.env.POSTGREST_URL || 'http://localhost:3000',
  auth: {
    persistSession: true,
    autoRefreshToken: true
  },
  cache: {
    enabled: true,
    ttl: 300000, // 5 minutes
    maxSize: 1000
  }
});

Dual Query Syntax ​

PGRestify supports two query approaches - choose what feels natural:

🎯 PostgREST Native Syntax ​

typescript
// Get users with array syntax (recommended)
const { data: users, error } = await client
  .from('users')
  .select(['id', 'name', 'email', 'active', 'created_at'])
  .execute();

// Query with aliases using array syntax
const { data: usersWithAliases } = await client
  .from('users')
  .select([
    'id AS user_id',
    'name AS full_name', 
    'email AS contact_email',
    'created_at AS signup_date'
  ])
  .execute();

// Filter with multiple sort orders
const { data: activeUsers } = await client
  .from('users')
  .select(['id', 'name', 'email', 'role'])
  .eq('active', true)
  .order('role')                           // Primary sort: by role
  .order('name', { ascending: true })      // Secondary sort: alphabetical
  .execute();

// Relations example with aliases and sorting
const { data: usersWithPosts } = await client
  .from('users')
  .select([
    'id',
    'name AS user_name',
    'posts.title AS post_title',
    'posts.created_at AS post_date'
  ])
  .relations(['posts'])
  .eq('active', true)
  .order('name')
  .order('posts.created_at', { ascending: false })
  .execute();

🏗️ Repository Pattern ​

typescript
// Get repository for users table
const userRepo = client.getRepository<User>('users');

// Simple queries
const users = await userRepo.find();
const activeUsers = await userRepo.findBy({ active: true });
const user = await userRepo.findOne({ id: 1 });

// Query builder with aliases
const usersWithAliases = await userRepo
  .createQueryBuilder()
  .select([
    'id AS user_id',
    'name AS full_name',
    'email AS contact_email'
  ])
  .where('active = :active', { active: true })
  .orderBy('name', 'ASC')
  .getMany();

// Multiple sort orders with repository pattern
const sortedUsers = await userRepo
  .createQueryBuilder()
  .select(['id', 'name', 'email', 'role', 'created_at'])
  .where('active = :active', { active: true })
  .orderBy('role', 'ASC')              // Primary sort
  .addOrderBy('created_at', 'DESC')    // Secondary sort
  .addOrderBy('name', 'ASC')           // Tertiary sort
  .getMany();

// Relations with repository pattern  
const usersWithRelations = await userRepo
  .createQueryBuilder()
  .select([
    'id',
    'name AS user_name',
    'posts.title AS post_title',
    'posts.published'
  ])
  .relations(['posts'])
  .where('active = :active', { active: true })
  .orderBy('name', 'ASC')
  .getMany();

CRUD Operations ​

Create (Insert) Records ​

typescript
// Insert a single user
const { data: newUser } = await client
  .from('users')
  .insert({
    name: 'John Doe',
    email: 'john@example.com',
    active: true
  })
  .single()
  .execute();

// Bulk insert users
const { data: newUsers } = await client
  .from('users')
  .insert([
    { name: 'Alice', email: 'alice@example.com', active: true },
    { name: 'Bob', email: 'bob@example.com', active: false }
  ])
  .execute();
typescript
// Create single user
const user = await userRepo.save({
  name: 'John Doe',
  email: 'john@example.com',
  active: true
});

// Create multiple users
const users = await userRepo.save([
  { name: 'Alice', email: 'alice@example.com', active: true },
  { name: 'Bob', email: 'bob@example.com', active: false }
]);

Read (Select) Records ​

typescript
// Find all active users
const { data: activeUsers } = await client
  .from('users')
  .select('*')
  .eq('active', true)
  .execute();

// Find a specific user
const { data: user } = await client
  .from('users')
  .select('id, name, email')
  .eq('id', 1)
  .single()
  .execute();

// Complex query with joins
const { data: usersWithPosts } = await client
  .from('users')
  .select(`
    id, 
    name, 
    email, 
    posts:posts(id, title, content)
  `)
  .eq('active', true)
  .execute();
typescript
// Find all active users
const activeUsers = await userRepo.findBy({ active: true });

// Find a specific user
const user = await userRepo.findOne({ id: 1 });

// Advanced query with joins
const usersWithPosts = await userRepo
  .createQueryBuilder()
  .leftJoinAndSelect('posts', 'post')
  .where('active = :active', { active: true })
  .getMany();

Update Records ​

typescript
// Update a user
const { data: updatedUser } = await client
  .from('users')
  .update({ active: false })
  .eq('id', 1)
  .single()
  .execute();

// Bulk update
const { data: updatedUsers } = await client
  .from('users')
  .update({ active: false })
  .lt('created_at', '2023-01-01')
  .execute();
typescript
// Update a user
const user = await userRepo.findOne({ id: 1 });
if (user) {
  user.active = false;
  await userRepo.save(user);
}

// Direct update
await userRepo.update({ id: 1 }, { active: false });

// Bulk update
await userRepo.update(
  { created_at: { $lt: '2023-01-01' } }, 
  { active: false }
);

Delete Records ​

typescript
// Delete a specific user
await client
  .from('users')
  .delete()
  .eq('id', 1)
  .execute();

// Bulk delete
await client
  .from('users')
  .delete()
  .eq('active', false)
  .execute();
typescript
// Delete a specific user
const user = await userRepo.findOne({ id: 1 });
if (user) {
  await userRepo.remove(user);
}

// Direct delete
await userRepo.delete({ id: 1 });

// Bulk delete
await userRepo.delete({ active: false });

Advanced Querying ​

typescript
// Pagination
const { data: paginatedUsers, count } = await client
  .from('users')
  .select('*', { count: 'exact' })
  .order('created_at', { ascending: false })
  .range(0, 9)
  .execute();

// Full-text search
const { data: searchResults } = await client
  .from('posts')
  .select('*')
  .textSearch('content', 'typescript postgresql')
  .limit(10)
  .execute();

// Aggregate functions
const { data: userStats } = await client
  .from('users')
  .select(`
    count(*) as total_users,
    avg(id) as avg_id,
    min(created_at) as first_user_date
  `)
  .single()
  .execute();
typescript
// Pagination with repository
const [users, total] = await userRepo
  .createQueryBuilder()
  .orderBy('created_at', 'DESC')
  .limit(10)
  .offset(0)
  .getManyAndCount();

// Full-text search with repository
const searchResults = await userRepo
  .createQueryBuilder()
  .where('name ILIKE :search', { search: '%john%' })
  .orWhere('email ILIKE :search', { search: '%john%' })
  .getMany();

// Complex filtering
const complexQuery = await userRepo
  .createQueryBuilder()
  .where('active = :active', { active: true })
  .andWhere('created_at >= :date', { date: '2024-01-01' })
  .andWhere('role IN (:...roles)', { roles: ['admin', 'moderator'] })
  .orderBy('name', 'ASC')
  .getMany();

Error Handling ​

typescript
// PostgREST syntax error handling
try {
  const { data: user, error } = await client
    .from('users')
    .select('*')
    .eq('id', 999)
    .single()
    .execute();
    
  if (error) {
    console.error('Database error:', error);
  } else {
    console.log('User found:', user);
  }
} catch (err) {
  console.error('Network error:', err);
}

// Repository pattern error handling
try {
  const user = await userRepo.findOne({ id: 999 });
  if (!user) {
    console.log('User not found');
  }
} catch (error) {
  if (error.name === 'PostgrestError') {
    console.error('Database error:', error.message);
  } else {
    console.error('Unexpected error:', error);
  }
}

Real-world Example: User Management System ​

typescript
class UserService {
  private userRepo = client.getRepository<User>('users');

  // Get active users with pagination
  async getActiveUsers(page = 1, limit = 10) {
    const offset = (page - 1) * limit;
    
    return await this.userRepo
      .createQueryBuilder()
      .where('active = :active', { active: true })
      .orderBy('created_at', 'DESC')
      .limit(limit)
      .offset(offset)
      .getManyAndCount();
  }

  // Search users by name or email
  async searchUsers(query: string) {
    return await this.userRepo
      .createQueryBuilder()
      .where('name ILIKE :query', { query: `%${query}%` })
      .orWhere('email ILIKE :query', { query: `%${query}%` })
      .andWhere('active = :active', { active: true })
      .getMany();
  }

  // Create user with validation
  async createUser(userData: Partial<User>) {
    // Check if email already exists
    const existingUser = await this.userRepo.findOne({ 
      email: userData.email 
    });
    
    if (existingUser) {
      throw new Error('Email already exists');
    }

    return await this.userRepo.save({
      ...userData,
      created_at: new Date().toISOString()
    });
  }

  // Update user profile
  async updateUser(id: number, updates: Partial<User>) {
    const user = await this.userRepo.findOne({ id });
    
    if (!user) {
      throw new Error('User not found');
    }

    return await this.userRepo.save({
      ...user,
      ...updates,
      updated_at: new Date().toISOString()
    });
  }

  // Soft delete user
  async deactivateUser(id: number) {
    return await this.userRepo.update(
      { id },
      { active: false, updated_at: new Date().toISOString() }
    );
  }
}

// Usage
const userService = new UserService();

// Get paginated active users
const [users, total] = await userService.getActiveUsers(1, 20);

// Search for users
const searchResults = await userService.searchUsers('john');

// Create new user
const newUser = await userService.createUser({
  name: 'Jane Doe',
  email: 'jane@example.com',
  role: 'user',
  active: true
});

Advanced Examples: Combining All Features ​

Here are practical examples that combine relations, aliases, and multiple sort orders:

typescript
// E-commerce: Products with categories and reviews
const getProductCatalog = async () => {
  return client
    .from('products')
    .select([
      'id AS product_id',
      'name AS product_name',
      'price AS current_price',
      'category.name AS category_name',
      'reviews.rating AS review_rating',
      'reviews.count AS review_count'
    ])
    .relations(['category', 'reviews'])
    .eq('active', true)
    .order('category.name')                    // Group by category
    .order('reviews.rating', { ascending: false })  // Best rated first
    .order('price')                            // Cheapest first
    .execute();
};

// Blog: Posts with authors and comments
const getBlogPosts = async () => {
  return client
    .from('posts')
    .select([
      'id AS post_id',
      'title AS post_title',
      'created_at AS published_date',
      'author.name AS author_name',
      'author.email AS author_contact',
      'comments.content AS comment_text',
      'comments.created_at AS comment_date'
    ])
    .relations(['author', 'comments'])
    .eq('published', true)
    .order('created_at', { ascending: false })     // Latest posts first
    .order('comments.created_at', { ascending: false })  // Latest comments first
    .execute();
};

// User Management: Users with profiles and recent activity
const getUserDashboard = async () => {
  return client
    .from('users')
    .select([
      'id AS user_id',
      'name AS display_name',
      'email AS contact_email',
      'created_at AS joined_date',
      'profile.bio AS user_bio',
      'profile.avatar_url AS profile_image',
      'activity.action AS recent_action',
      'activity.created_at AS activity_date'
    ])
    .relations(['profile', 'activity'])
    .eq('active', true)
    .order('activity.created_at', { ascending: false })  // Recent activity first
    .order('created_at', { ascending: false })     // Newest users first
    .order('name')                                 // Alphabetical fallback
    .limit(50)
    .execute();
};

// Repository Pattern: Complex business queries
const userRepo = client.getRepository<User>('users');

const getTeamDirectory = async () => {
  return userRepo
    .createQueryBuilder()
    .select([
      'id AS employee_id',
      'first_name AS firstName',
      'last_name AS lastName',
      'email AS workEmail',
      'department.name AS dept_name',
      'manager.first_name AS manager_firstName',
      'manager.last_name AS manager_lastName'
    ])
    .relations(['department', 'manager'])
    .where('active = :active', { active: true })
    .orderBy('department.name', 'ASC')             // Group by department
    .addOrderBy('manager.last_name', 'ASC')        // Then by manager
    .addOrderBy('last_name', 'ASC')                // Then alphabetical
    .addOrderBy('first_name', 'ASC')               // Finally by first name
    .getMany();
};

Best Practices ​

Type Safety ​

typescript
// Always define your types
interface User {
  id: number;
  name: string;
  email: string;
  // ... other fields
}

// Use generics for type safety
const userRepo = client.getRepository<User>('users');
const user = await userRepo.findOne({ id: 1 }); // Type: User | null

Error Handling ​

typescript
// Always handle errors appropriately
try {
  const result = await userRepo.find();
  return result;
} catch (error) {
  console.error('Failed to fetch users:', error);
  throw error; // Re-throw or handle appropriately
}

Performance Optimization ​

typescript
// Select only needed fields
const users = await client
  .from('users')
  .select('id, name, email') // Don't select unnecessary fields
  .execute();

// Use pagination for large datasets
const paginatedUsers = await userRepo
  .createQueryBuilder()
  .limit(50) // Reasonable page size
  .offset(page * 50)
  .getMany();

// Use caching for frequently accessed data
const client = createClient({
  cache: {
    enabled: true,
    ttl: 300000 // 5 minutes
  }
});

Security Considerations ​

typescript
// Validate input parameters
function validateUserId(id: unknown): number {
  const userId = Number(id);
  if (!Number.isInteger(userId) || userId <= 0) {
    throw new Error('Invalid user ID');
  }
  return userId;
}

// Use parameterized queries to prevent injection
const users = await userRepo
  .createQueryBuilder()
  .where('name = :name', { name: userInput }) // Safe parameterized query
  .getMany();

This example demonstrates:

  • ✅ Both PostgREST native syntax and repository patterns
  • ✅ Complete CRUD operations with both approaches
  • ✅ Advanced querying techniques
  • ✅ Proper error handling
  • ✅ Real-world service architecture
  • ✅ Type safety best practices
  • ✅ Performance optimization tips
  • ✅ Security considerations

Released under the MIT License.