Learn how to write data to the Supabase database

In this page we learn how to write data to the Supabase database in your Next.js app

In this page, we will learn how to write data to the Supabase database in your Next.js app.

Writing a Server Action to Add a Task

Server Actions are defined by adding use server at the top of the function or file. When we define a function as a Server Action, it will be executed on the server-side.

This is useful for various reasons:

  1. By using Server Actions, we can revalidate data fetched through Server Components
  2. We can execute server side code just by calling the function from the client side

In this example, we will write a Server Action to add a task to the database.

Defining a Schema for the Task

We use Zod to validate the data that is passed to the Server Action. This ensures that the data is in the correct format before it is written to the database.

The convention in Makerkit is to define the schema in a separate file and import it where needed. We use the convention file.schema.ts to define the schema.

import { z } from 'zod';
export const WriteTaskSchema = z.object({
title: z.string().min(1),
description: z.string().nullable(),
});

Writing the Server Action to Add a Task

In this example, we write a Server Action to add a task to the database. We use the revalidatePath function to revalidate the /home page after the task is added.

'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';
import { getLogger } from '@kit/shared/logger';
import { requireUser } from '@kit/supabase/require-user';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { WriteTaskSchema } from '~/(dashboard)/home/(user)/_lib/schema/write-task.schema';
export async function addTaskAction(params: z.infer<typeof WriteTaskSchema>) {
'use server';
const task = WriteTaskSchema.parse(params);
const logger = await getLogger();
const client = getSupabaseServerClient();
const auth = await requireUser(client);
if (!auth.data) {
redirect(auth.redirectTo);
}
logger.info(task, `Adding task...`);
const { data, error } = await client
.from('tasks')
.insert({ ...task, account_id: auth.data.id });
if (error) {
logger.error(error, `Failed to add task`);
throw new Error(`Failed to add task`);
}
logger.info(data, `Task added successfully`);
revalidatePath('/home', 'page');
return null;
}

Let's focus on this bit for a second:

const { data, error } = await client
.from('tasks')
.insert({ ...task, account_id: auth.data.id });

Do you see the account_id field? This is a foreign key that links the task to the user who created it. This is a common pattern in database design.

Now that we have written the Server Action to add a task, we can call this function from the client side. But we need a form, which we define in the next section.

Creating a Form to Add a Task

We create a form to add a task. The form is a React component that accepts a SubmitButton prop and an onSubmit prop.

import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Textarea } from '@kit/ui/textarea';
import { Trans } from '@kit/ui/trans';
import { WriteTaskSchema } from '../_lib/schema/write-task.schema';
export function TaskForm(props: {
task?: z.infer<typeof WriteTaskSchema>;
onSubmit: (task: z.infer<typeof WriteTaskSchema>) => void;
SubmitButton: React.ComponentType;
}) {
const form = useForm({
resolver: zodResolver(WriteTaskSchema),
defaultValues: props.task,
});
return (
<Form {...form}>
<form
className={'flex flex-col space-y-4'}
onSubmit={form.handleSubmit(props.onSubmit)}
>
<FormField
render={(item) => {
return (
<FormItem>
<FormLabel>
<Trans i18nKey={'tasks:taskTitle'} />
</FormLabel>
<FormControl>
<Input required {...item.field} />
</FormControl>
<FormDescription>
<Trans i18nKey={'tasks:taskTitleDescription'} />
</FormDescription>
<FormMessage />
</FormItem>
);
}}
name={'title'}
/>
<FormField
render={(item) => {
return (
<FormItem>
<FormLabel>
<Trans i18nKey={'tasks:taskDescription'} />
</FormLabel>
<FormControl>
<Textarea {...item.field} />
</FormControl>
<FormDescription>
<Trans i18nKey={'tasks:taskDescriptionDescription'} />
</FormDescription>
<FormMessage />
</FormItem>
);
}}
name={'description'}
/>
<props.SubmitButton />
</form>
</Form>
);
}

Using a Dialog component to display the form

We use the Dialog component from the @kit/ui/dialog package to display the form in a dialog. The dialog is opened when the user clicks on a button.

'use client';
import { useState, useTransition } from 'react';
import { PlusCircle } from 'lucide-react';
import { Button } from '@kit/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@kit/ui/dialog';
import { Trans } from '@kit/ui/trans';
import { TaskForm } from '../_components/task-form';
import { addTaskAction } from '../_lib/server/server-actions';
export function NewTaskDialog() {
const [pending, startTransition] = useTransition();
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button>
<PlusCircle className={'mr-1 h-4'} />
<span>
<Trans i18nKey={'tasks:addNewTask'} />
</span>
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans i18nKey={'tasks:addNewTask'} />
</DialogTitle>
<DialogDescription>
<Trans i18nKey={'tasks:addNewTaskDescription'} />
</DialogDescription>
</DialogHeader>
<TaskForm
SubmitButton={() => (
<Button>
{pending ? (
<Trans i18nKey={'tasks:addingTask'} />
) : (
<Trans i18nKey={'tasks:addTask'} />
)}
</Button>
)}
onSubmit={(data) => {
startTransition(async () => {
await addTaskAction(data);
setIsOpen(false);
});
}}
/>
</DialogContent>
</Dialog>
);
}

We can now import NewTaskDialog in the /home page and display the dialog when the user clicks on a button.

Let's go back to the home page and add the component right next to the input filter:

<div className={'flex items-center justify-between'}>
<div>
<Heading level={4}>
<Trans i18nKey={'tasks:tasksTabLabel'} defaults={'Tasks'} />
</Heading>
</div>
<div className={'flex items-center space-x-2'}>
<form className={'w-full'}>
<Input
name={'query'}
defaultValue={query}
className={'w-full lg:w-[18rem]'}
placeholder={'Search tasks'}
/>
</form>
<NewTaskDialog />
</div>
</div>