Learn how to write data to the Supabase database in your Tanstack Start app
In this page we learn how to write data to the Supabase database in your Tanstack Start app
In this page, we will learn how to write data to the Supabase database in your Tanstack Start app.
How to write data to the Supabase database
In this page we learn how to write data to the Supabase database in your Tanstack Start app
Writing a Server Function to Add a Task
Server functions are defined with createServerFn from @tanstack/react-start. The handler runs on the server; the TanStack Start compiler strips it (and the Supabase/server graph it pulls) from the client bundle.
This is useful for various reasons:
- We can execute server-side code just by calling the function from the client
- Authorization is composed from middleware —
authFunctionMiddlewareinjectscontext.user
In this example, we will write a server function to add a task to the database. By convention it lives in a *.functions.ts file and the export is suffixed with Function.
Defining a Schema for the Task
We use Zod to validate the data that is passed to the server function. 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 * as z from 'zod';export const WriteTaskSchema = z.object({ title: z.string().min(1), description: z.string().nullable(),});Writing the Server Function to Add a Task
In this example, we write a server function to add a task to the database. There is no revalidatePath: after the mutation succeeds the client calls router.invalidate() to refetch the affected route loaders (shown further down).
import { createServerFn } from '@tanstack/react-start';import { authFunctionMiddleware } from '@kit/function-middleware/functions';import { getLogger } from '@kit/shared/logger';import { getSupabaseServerClient } from '@kit/supabase/server-client';import { WriteTaskSchema } from '../schema/write-task.schema';export const addTaskFunction = createServerFn({ method: 'POST' }) .middleware(authFunctionMiddleware) .validator(WriteTaskSchema) .handler(async ({ data: task, context: { user } }) => { const logger = await getLogger(); const client = getSupabaseServerClient(); logger.info(task, `Adding task...`); const { error } = await client .from('tasks') .insert({ ...task, account_id: user.id }); if (error) { logger.error(error, `Failed to add task`); throw new Error(`Failed to add task`); } logger.info(`Task added successfully`); return { success: true }; });Let's focus on this bit for a second:
const { error } = await client .from('tasks') .insert({ ...task, account_id: user.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 function to add a task, we can call it 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 * as 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.
import { useState } from 'react';import { useMutation } from '@tanstack/react-query';import { useRouter } from '@tanstack/react-router';import { useServerFn } from '@tanstack/react-start';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 { addTaskFunction } from '../server/add-task.functions';export function NewTaskDialog() { const router = useRouter(); const addTask = useServerFn(addTaskFunction); const [isOpen, setIsOpen] = useState(false); const mutation = useMutation({ mutationFn: (task: z.infer<typeof WriteTaskSchema>) => addTask({ data: task }), onSuccess: () => { setIsOpen(false); return router.invalidate(); }, }); const pending = mutation.isPending; 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) => mutation.mutate(data)} /> </DialogContent> </Dialog> );}We can now import NewTaskDialog in the /dashboard 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>