Next.js - February 16, 2024

Writing Secure Next.js Server Actions

5 minute read

It's not a surprise that I'm a big Next.js fan. Especially working with Server Actions is a great experience. However, in my opinion, there's a crucial aspect of security that is way too often overlooked. To contribute my part to the Next.js community, I've created a package called next-server-action-validation which aims to solve this problem. In this article, I'll explain the motivation behind creating this package and how to use it to secure your Next.js Server Actions.

The Birth of Next.js Server Actions

Before we dive into the action (see what I did there?), let's take a step back and look at the (short) history of Next.js Server Actions.

Traditionally, communicating with the server-side in Next.js required creating an API route. However, with the release of Next.js 13 in October 2022, Vercel introduced an experimental concept known as Server Actions.

A year later, in October 2023, the launch of Next.js 14 marked Server Actions as a stable feature. The initial reception of Server Actions was not great, resulting in a wave of memes and jokes about how 'ridiculous' the feature was. One particular image from the Next.js 14 presentation was widely circulated.

Vercel Presentation Next.js Server Actions

For many developers, the idea of integrating SQL within a ReactJS component seemed absurd. And indeed, from a developer's perspective, I concur. However, I believe the criticism Server Actions received was not entirely justified.

Obviously, during the Vercel presentation, they used this piece of code to give an example of what Server Actions are and how you can use them. In my opinion, they did a great job in shipping the message.

A few months later, I absolutely fell in love with Server Actions. When I need to pass data from the client to my server, I simply create a function annotated with the use server flag and call it from my client component. This eliminates the need for a full API endpoint, greatly enhancing code readability and development speed. However there's one critical aspect of Server Actions that is often overlooked.

The Problem With Server Actions

So what's the problem with Server Actions? As I mentioned earlier, the problem a lot of developers overlook is the security of their Server Actions. Since Server Actions are essentially compiled into 'regular' API routes by Next.js, it's crucial to validate all incoming data before it flows into your server side logic.

Many developers use Zod to validate input data on the front end before sending it to the backend via a Server Action. While this is a good practice, it leaves your Server Actions —or essentially your API routes— still vulnerable to invalid data, allowing bad-actors to directly send any data they wish to the server side of your application.

Implementing (copying) the same Zod schema from your client component into your server action is a solution. However, as lazy developers, we do not want to repeat ourselves (keep it DRY!).

The Solution

The solution to this problem is to migrate the Zod schema, which validates user input, directly into your Server Action and ship all validation errors back to your Client Component. This results into your Server Action being fully secure, while still providing the capability to display appropriate input validation errors on the front-end based on the Zod results. This is exactly what next-server-action-validation solves.

Implementing next-server-action-validation

So how do you use next-server-action-validation? It's quite simple.

Installation

npm install next-server-action-validation zod

Usage

As mentioned, we use Zod for the runtime validation of data that that gets passed into our Server Actions. This package provides a function called withValidation which wraps your Server Action and validates the data passed into it.

Step 1: Protect your Server Action

To protect your server action it needs to be wrapped in the withValidation function together with a Zod.js validation schema.

// server-action.ts
'use server';
 
import { withValidation } from 'next-server-action-validation';
import * as z from 'zod';
 
// 1. Create the Validation Schema
const myServerActionSchema = z.object({
	name: z.string().min(4)
});
 
// 2. Wrap your server action in the `withValidation` function
export const myServerAction = withValidation(
	myServerActionSchema,
	async (data: z.infer<typeof myServerActionSchema>) => {
		console.log(data); // Data is fully typesafe
		return true;
	}
);

Step 2: Use your Server Action

To use your server action and check for validation errors we can simply call our action and pass its result into the isValidationError function. If this function resolves to be truthy the validation failed. From now on the type of our result is a ValidationError which holds all validation errors.

// page.tsx
'use client';
 
import { useState } from 'react';
import { isValidationError } from 'next-server-action-validation';
import { myServerAction } from '@/app/page.actions';
 
export default function Home() {
	const [myString, setMyString] = useState('');
 
	async function handleSave() {
		// 1. Call your server action as normal
		const result = await myServerAction({ name: myString });
 
		// 2. Check for validation errors
		if (isValidationError(result)) {
			// The type of result.errors is an array of `ZodIssues`
			// which we can use to display proper error messages
			console.log(result.errors);
 
			// 3. Return out of the function
			return;
		}
 
		// 4. Proceed as normal
		console.log('We passed correct data!');
	}
 
	return (
		<main>
			<Input onChange={(e) => setMyString(e.target.value)} />
			<Button onClick={handleSave}>Save</Button>
		</main>
	);
}

Conclusion

In conclusion, next-server-action-validation bridges a crucial gap in Next.js Server Actions by securely integrating Zod schema validation directly into server-side processing. This approach not only fortifies your Server Actions against invalid data but also maintains the ease of displaying validation errors on the client side.

By using this package, you can enhance your Next.js applications' security without sacrificing development speed or violating the DRY principle. It's a step forward in making full-stack development with Next.js more robust and efficient.