Next.js Server Actions let you write and execute server-side code directly in your React components, simply by calling them. They are a very powerful feature, and they can be used for a lot of different use cases.
But handling authentication, validation, and type safety properly can be tricky. In this guide, we'll explore how to build secure and type-safe Server Actions using Makerkit's enhanceAction
utility, a special function we reuse in our Next.js SaaS Boilerlate for safely calling server actions.
What Makes a Good Server Action?
A well-built Server Action should:
- Validate input data
- Check authentication when needed
- Handle errors gracefully
- Provide type safety
- Protect against abuse
Let's see how enhanceAction
helps with all of these, and how you can write your own version of enhanceAction
to fit your needs.
Basic Example - A Server Action for Creating a Task
Here's a simple Server Action for creating a task:
'use server';import { enhanceAction } from '@kit/next/actions';import { z } from 'zod';const TaskSchema = z.object({ title: z.string().min(1, "Title is required"), priority: z.enum(["low", "medium", "high"])});export const createTask = enhanceAction( async (data, user) => { const client = getSupabaseServerActionClient(); const { error } = await client .from('tasks') .insert({ ...data, user_id: user.id }); if (error) { throw new Error('Failed to create task'); } return { success: true }; }, { schema: TaskSchema, auth: true });
In the above code, we validate the input data using Zod, and we require authentication.
When we define the body of the Server Action, we can use user
to access the authenticated user, and data
to access the input data. Both are well typed and ready-to-use, without additional verification.
Key Features of "enhanceAction" for Server Actions
1. Authentication
Simply set auth: true
to require authentication:
export const protectedAction = enhanceAction( async (data, user) => { // user is guaranteed to exist console.log(`Action by user ${user.id}`); return { success: true }; }, { auth: true });
This is true
by default, however you can set it to false
if you don't want to require authentication.
export const publicAction = enhanceAction( async (data) => { console.log('This action is public'); return { success: true }; }, { auth: false });
2. Input Validation with Zod
Define your schema and enhanceAction
handles validation:
const ContactSchema = z.object({ email: z.string().email(), message: z.string().min(10)});export const submitContact = enhanceAction( async (data) => { // data is typed according to ContactSchema await sendEmail(data.email, data.message); }, { schema: ContactSchema });
If the input data doesn't match the schema, enhanceAction
will throw an error without executing the action body.
You can omit this property if you don't need validation, however data
will be of type any
in that case.
3. Captcha Protection
Need spam protection? Enable captcha:
export const submitForm = enhanceAction( async (data) => { // Form submission logic }, { captcha: true, schema: FormSchema });
Use it in your component:
import { useCaptchaToken } from '@kit/auth/captcha/client';function Form() { const { captchaToken } = useCaptchaToken(); const onSubmit = async (data) => { await submitForm({ ...data, captchaToken }); }; return <form onSubmit={handleSubmit(onSubmit)}>...</form>;}
These functions require some code that you're not seeing here, but you can find it in the public SaaS Boilerplate by Makerkit. which you can use for free to learn and build your own SaaS with Next.js and Supabase.
The captcha protection uses Turnstile by Cloudflare, a free service that is easy to set up and easy to use.
Using with React Hook Form
React Hook Form is the most popular form library for React. It is also used by Shadcn UI for its form components, and therefore it's also baked into Makerkit itself.
Here's how to use it with React Hook Form:
function TaskForm() { const form = useForm({ resolver: zodResolver(TaskSchema) }); const [isPending, startTransition] = useTransition(); return ( <Form {...form}> <form onSubmit={form.handleSubmit((data) => { startTransition(async () => { try { await createTask(data); form.reset(); } catch (error) { console.error('Failed to create task:', error); } }); })}> <FormField control={form.control} name="title" render={({ field }) => ( <FormItem> <FormLabel>Title</FormLabel> <FormControl> <Input {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <Button type="submit" disabled={isPending}> {isPending ? 'Creating...' : 'Create Task'} </Button> </form> </Form> );}
Error Handling
The enhanceAction
utility provides consistent error handling:
export const riskyAction = enhanceAction( async (data) => { const result = await someOperation(); if (!result.success) { throw new Error('Operation failed'); } return result; }, { schema: MySchema });// In your component:try { await riskyAction(data);} catch (error) { // Handle error}
Best Practices
Follow the below best practices for writing secure and type-safe Server Actions:
- Always Validate Input
- Define Zod schemas for all your actions
- Validate on both client and server
- This also ensures proper type-safety
- Handle Authentication Properly
- Use
auth: true
for protected actions - Never trust client-side data
- Use Captcha for spam protection
- Use
- Error Handling
- Always catch errors in components
- Provide meaningful error messages
- Consider using toast notifications
- Type Safety
- Take advantage of TypeScript
- Let Zod handle runtime validation
- Performance
- Use
useTransition
for better loading states - Consider optimistic updates for better UX
- Use
The enhanceAction
utility is a powerful tool for writing secure and type-safe Server Actions in Next.js - but it may not fit your needs. If you have specific requirements, you can always write your own version of enhanceAction
and improve upon it.
Source Code
The complete source code for the enhanceAction
utility is available on GitHub.
Makerkit's implementation uses:
- Zod for input validation
- Supabase for Authentication
- Cloudflare Turnstile for Captcha protection
You can use this code as a starting point for your own version, or if you use the same technologies, you can simply re-use it.
Conclusion
With enhanceAction
, you can write secure and type-safe Server Actions without the boilerplate. It handles authentication, validation, and error handling, letting you focus on your business logic.
Remember:
- Use Server Actions for mutations and form submissions
- Consider Server Components for data fetching
- Use Route Handlers for public APIs
If you're unsure when to write Server Actions or Route Handlers, we have an article about when to use Server Actions vs Route Handlers that will help you decide.
By following these patterns, you'll build more secure and maintainable applications with Next.js Server Actions.
Makerkit - the leading Next.js SaaS Boilerplate 💡
If you liked this article, you can check out our Next.js SaaS Boilerplate - a trusted and battle-tested Next.js SaaS starter kit that helps you build secure and maintainable SaaS applications.