Bỏ qua

Supabase JavaScript Client

Cài đặt và Setup

Installation

# npm
npm install @supabase/supabase-js

# yarn
yarn add @supabase/supabase-js

# pnpm
pnpm add @supabase/supabase-js

Basic Setup

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  'https://your-project.supabase.co',
  'your-anon-key'
);

With TypeScript Types

import { createClient } from '@supabase/supabase-js';
import { Database } from './types/supabase';

const supabase = createClient<Database>(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

// Now you get type hints for tables and columns
const { data } = await supabase
  .from('tasks')  // Autocomplete!
  .select('id, title, status');

Generate TypeScript Types

Using CLI

# Generate types from remote database
supabase gen types typescript --project-id your-project-id > types/supabase.ts

# Generate from local database
supabase gen types typescript --local > types/supabase.ts

# In package.json
{
  "scripts": {
    "gen:types": "supabase gen types typescript --project-id $PROJECT_ID > types/supabase.ts"
  }
}

Generated Types Structure

// types/supabase.ts
export type Database = {
  public: {
    Tables: {
      tasks: {
        Row: {
          id: string;
          title: string;
          status: string;
          user_id: string;
          created_at: string;
        };
        Insert: {
          id?: string;
          title: string;
          status?: string;
          user_id: string;
          created_at?: string;
        };
        Update: {
          id?: string;
          title?: string;
          status?: string;
          user_id?: string;
          created_at?: string;
        };
      };
    };
  };
};

Query Patterns

Select

// Basic select
const { data, error } = await supabase
  .from('tasks')
  .select('*');

// Select specific columns
const { data } = await supabase
  .from('tasks')
  .select('id, title, status');

// With relations
const { data } = await supabase
  .from('tasks')
  .select(`
    id,
    title,
    project:projects(name),
    assignee:profiles(full_name, avatar_url)
  `);

// Count only
const { count } = await supabase
  .from('tasks')
  .select('*', { count: 'exact', head: true });

Filters

// Equal
.eq('status', 'active')

// Not equal
.neq('status', 'deleted')

// Greater/Less than
.gt('priority', 3)
.gte('priority', 3)
.lt('priority', 3)
.lte('priority', 3)

// Pattern matching
.like('title', '%task%')
.ilike('title', '%Task%')  // Case insensitive

// In array
.in('status', ['active', 'pending'])

// Contains (array column)
.contains('tags', ['urgent'])

// Is null
.is('deleted_at', null)

// Range
.range(0, 9)  // First 10 records

// Combine with AND
.eq('status', 'active')
.gt('priority', 2)

// OR conditions
.or('status.eq.active,status.eq.pending')

Insert

// Single insert
const { data, error } = await supabase
  .from('tasks')
  .insert({
    title: 'New Task',
    status: 'todo',
    user_id: userId,
  })
  .select()
  .single();

// Multiple insert
const { data, error } = await supabase
  .from('tasks')
  .insert([
    { title: 'Task 1', user_id: userId },
    { title: 'Task 2', user_id: userId },
  ])
  .select();

// Upsert (insert or update)
const { data, error } = await supabase
  .from('tasks')
  .upsert({
    id: existingId,  // If exists, update
    title: 'Updated Title',
  })
  .select()
  .single();

Update

// Update by ID
const { data, error } = await supabase
  .from('tasks')
  .update({ status: 'done' })
  .eq('id', taskId)
  .select()
  .single();

// Update multiple
const { data, error } = await supabase
  .from('tasks')
  .update({ status: 'archived' })
  .eq('project_id', projectId)
  .lt('updated_at', '2024-01-01')
  .select();

Delete

// Delete by ID
const { error } = await supabase
  .from('tasks')
  .delete()
  .eq('id', taskId);

// Delete multiple
const { error } = await supabase
  .from('tasks')
  .delete()
  .eq('status', 'deleted')
  .lt('deleted_at', thirtyDaysAgo);

Ordering & Pagination

Order

// Single column
const { data } = await supabase
  .from('tasks')
  .select('*')
  .order('created_at', { ascending: false });

// Multiple columns
const { data } = await supabase
  .from('tasks')
  .select('*')
  .order('priority', { ascending: false })
  .order('created_at', { ascending: false });

// Null handling
const { data } = await supabase
  .from('tasks')
  .select('*')
  .order('due_date', { ascending: true, nullsFirst: false });

Pagination

// Offset pagination
const page = 1;
const pageSize = 10;

const { data, count } = await supabase
  .from('tasks')
  .select('*', { count: 'exact' })
  .range((page - 1) * pageSize, page * pageSize - 1);

// Cursor pagination (better for large datasets)
const { data } = await supabase
  .from('tasks')
  .select('*')
  .gt('id', lastSeenId)
  .order('id')
  .limit(10);

RPC (Remote Procedure Call)

Call Database Functions

// Call function without params
const { data, error } = await supabase.rpc('get_active_users');

// Call with parameters
const { data, error } = await supabase.rpc('get_user_tasks', {
  user_uuid: userId,
  status_filter: 'active',
});

// Function returning single row
const { data, error } = await supabase
  .rpc('get_user_stats', { user_uuid: userId })
  .single();

Error Handling

Handle Errors

const { data, error } = await supabase
  .from('tasks')
  .select('*');

if (error) {
  console.error('Error code:', error.code);
  console.error('Error message:', error.message);
  console.error('Error details:', error.details);
  console.error('Error hint:', error.hint);

  // Handle specific errors
  if (error.code === 'PGRST116') {
    // No rows returned
  }
  if (error.code === '23505') {
    // Unique constraint violation
  }
  if (error.code === '42501') {
    // RLS violation
  }

  throw new Error(error.message);
}

// Use data safely
console.log(data);

TypeScript Error Handling

// Type-safe error handling
type QueryResult<T> = {
  data: T | null;
  error: Error | null;
};

async function getTasks(): Promise<Task[]> {
  const { data, error } = await supabase
    .from('tasks')
    .select('*');

  if (error) throw error;
  return data ?? [];
}

Client Configuration

Advanced Options

const supabase = createClient<Database>(url, key, {
  auth: {
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: true,
  },
  global: {
    headers: {
      'x-custom-header': 'value',
    },
  },
  db: {
    schema: 'public',
  },
  realtime: {
    params: {
      eventsPerSecond: 10,
    },
  },
});

Tổng kết

Query Cheat Sheet

// CRUD
.select('*')
.insert({ ... })
.update({ ... })
.delete()

// Filters
.eq('col', value)
.neq('col', value)
.gt('col', value)
.gte('col', value)
.lt('col', value)
.lte('col', value)
.in('col', [values])
.is('col', null)

// Modifiers
.order('col', { ascending: false })
.limit(10)
.range(0, 9)
.single()

// RPC
.rpc('function_name', { params })

Best Practices

✅ Generate and use TypeScript types
✅ Handle errors properly
✅ Use .single() for expected single row
✅ Use select() after insert/update to get returned data
✅ Use RPC for complex operations

Q&A

  1. TypeScript types đã generate chưa?
  2. Error handling pattern hiện tại?
  3. Có dùng RPC functions không?