How to Write Secure and Type-Safe Server Actions in Next.js

A comprehensive guide to using enhanceAction in Next.js applications. Learn to implement authentication, Zod validation, and error handling in your Server Actions

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:

  1. Always Validate Input
    • Define Zod schemas for all your actions
    • Validate on both client and server
    • This also ensures proper type-safety
  2. Handle Authentication Properly
    • Use auth: true for protected actions
    • Never trust client-side data
    • Use Captcha for spam protection
  3. Error Handling
    • Always catch errors in components
    • Provide meaningful error messages
    • Consider using toast notifications
  4. Type Safety
    • Take advantage of TypeScript
    • Let Zod handle runtime validation
  5. Performance
    • Use useTransition for better loading states
    • Consider optimistic updates for better UX

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:

  1. Zod for input validation
  2. Supabase for Authentication
  3. 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.