Page 3: Data Management (CRUD) in Next.js (App Router)

3. Data Management (CRUD) in Next.js (App Router)

Interacting with your Supabase database from your Next.js application is straightforward. This section demonstrates how to perform basic CRUD (Create, Read, Update, Delete) operations using the Supabase client.

3.1. Setting Up Your Database Table

First, create a simple table in your Supabase SQL Editor. For example, a todos table:

create table todos (
  id uuid primary key default uuid_generate_v4(),
  user_id uuid references auth.users not null,
  task text,
  is_complete boolean default false,
  inserted_at timestamp with time zone default timezone('utc'::text, now()) not null
);

3.2. Reading Data (Server Component)

Fetch data in a Server Component. This is efficient as data is fetched on the server before rendering.

// app/todos/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'

export default async function TodosPage() {
  const supabase = createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    redirect('/login')
  }

  const { data: todos, error } = await supabase
    .from('todos')
    .select('*')
    .eq('user_id', user.id) // Filter by authenticated user
    .order('inserted_at', { ascending: true })

  if (error) {
    console.error('Error fetching todos:', error.message)
    return <div>Error loading todos.</div>
  }

  return (
    <div>
      <h1>Your Todos</h1>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            {todo.task} ({todo.is_complete ? 'Complete' : 'Pending'})
          </li>
        ))}
      </ul>
    </div>
  )
}

3.3. Creating Data (Client Component/Server Action)

For creating new data, you can use a client component or a Server Action. Server Actions are recommended for mutations in App Router.

// app/todos/create-todo.ts (Server Action)
'use server'

import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'

export async function createTodo(formData: FormData) {
  const supabase = createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    throw new Error('User not authenticated')
  }

  const task = formData.get('task') as string

  const { error } = await supabase
    .from('todos')
    .insert({ user_id: user.id, task })

  if (error) {
    console.error('Error creating todo:', error.message)
    throw error
  }

  revalidatePath('/todos') // Revalidate the todos page to show new data
}

// app/todos/page.tsx (within your Client Component form)
// ...
<form action={createTodo}>
  <input type="text" name="task" placeholder="New task" required />
  <button type="submit">Add Todo</button>
</form>
// ...

3.4. Updating & Deleting Data (Client Component/Server Action)

Similar to creating, updating and deleting are best handled with Server Actions for robustness and security.

// Example for updating (Server Action)
'use server'

import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'

export async function updateTodoStatus(todoId: string, isComplete: boolean) {
  const supabase = createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    throw new Error('User not authenticated')
  }

  const { error } = await supabase
    .from('todos')
    .update({ is_complete: isComplete })
    .eq('id', todoId)
    .eq('user_id', user.id) // Ensure only owner can update

  if (error) {
    console.error('Error updating todo:', error.message)
    throw error
  }

  revalidatePath('/todos')
}

These examples provide a solid foundation for managing data in your Next.js application with Supabase.