DEV Community

Apollo
Apollo

Posted on

Next.js 14 Server Actions: The Patterns Junior Devs Always Get Wrong

Next.js 14 Server Actions: The Patterns Junior Devs Always Get Wrong

Next.js 14 introduced Server Actions, a powerful feature that allows developers to execute server-side logic directly from client components. While Server Actions simplify many aspects of modern web development, they’re also prone to misuse—especially by junior developers. Over the past few months, I’ve noticed recurring patterns of mistakes that lead to bugs, poor user experiences, and technical debt. In this article, I’ll walk through these common pitfalls, explain how to avoid them, and provide practical examples to ensure you’re using Server Actions effectively.


1. Skipping Validation: The Silent Killer

One of the most common mistakes I see is neglecting proper input validation in Server Actions. Junior developers often assume that data sent from the client is trustworthy, but this assumption can lead to serious vulnerabilities like SQL injection, data corruption, or even crashes.

Consider this example:

async function createPost(formData) {
  const title = formData.get('title');
  const content = formData.get('content');

  await db.post.create({
    data: { title, content },
  });
}
Enter fullscreen mode Exit fullscreen mode

Here, there’s no validation for title or content. What happens if title is empty or content exceeds the database’s character limit?

To fix this, always validate inputs using libraries like zod or yup. Here’s an improved version:

import { z } from 'zod';

const PostSchema = z.object({
  title: "z.string().min(1).max(100),"
  content: z.string().min(10).max(5000),
});

async function createPost(formData) {
  const result = PostSchema.safeParse({
    title: "formData.get('title'),"
    content: formData.get('content'),
  });

  if (!result.success) {
    throw new Error('Invalid input data');
  }

  await db.post.create({
    data: result.data,
  });
}
Enter fullscreen mode Exit fullscreen mode

Lessons learned:

  • Always validate inputs on the server.
  • Use schema validation libraries to enforce constraints.
  • Handle validation errors gracefully.

2. Ignoring Error Boundaries: Unhandled Exceptions Crash Your App

Another common mistake is failing to handle errors properly in Server Actions. Without proper error boundaries, exceptions can crash your application or leave users in the dark about what went wrong.

Take this example:

async function deletePost(postId) {
  await db.post.delete({ where: { id: postId } });
}
Enter fullscreen mode Exit fullscreen mode

If postId doesn’t exist or the database connection fails, this function will throw an unhandled exception.

To fix this, wrap your Server Actions in error boundaries and provide meaningful feedback to users. Here’s how:

async function deletePost(postId) {
  try {
    await db.post.delete({ where: { id: postId } });
  } catch (error) {
    console.error('Failed to delete post:', error);
    throw new Error('Failed to delete post. Please try again.');
  }
}
Enter fullscreen mode Exit fullscreen mode

In your client component, handle the error gracefully:

function DeletePostButton({ postId }) {
  const handleDelete = async () => {
    try {
      await deletePost(postId);
      alert('Post deleted successfully!');
    } catch (error) {
      alert(error.message);
    }
  };

  return <button onClick={handleDelete}>Delete Post</button>;
}
Enter fullscreen mode Exit fullscreen mode

Lessons learned:

  • Always wrap Server Actions in try-catch blocks.
  • Log errors for debugging purposes.
  • Provide user-friendly error messages.

3. Overloading Server Actions: The Single Responsibility Principle

Junior developers often cram too much logic into a single Server Action, violating the Single Responsibility Principle (SRP). This makes the code harder to maintain, test, and debug.

For example:

async function handleFormSubmission(formData) {
  // Validate data
  const result = PostSchema.safeParse(formData);
  if (!result.success) throw new Error('Invalid data');

  // Save to database
  const post = await db.post.create({ data: result.data });

  // Send email notification
  await sendEmail(post.authorEmail, 'Your post was created!');

  // Log analytics
  await logAnalytics('post_created', post.id);
}
Enter fullscreen mode Exit fullscreen mode

This Server Action does too much: validation, database operations, email notifications, and analytics logging.

Instead, break it into smaller, reusable functions:

async function validatePost(formData) {
  const result = PostSchema.safeParse(formData);
  if (!result.success) throw new Error('Invalid data');
  return result.data;
}

async function createPost(data) {
  return await db.post.create({ data });
}

async function handleFormSubmission(formData) {
  const data = await validatePost(formData);
  const post = await createPost(data);
  await sendEmail(post.authorEmail, 'Your post was created!');
  await logAnalytics('post_created', post.id);
}
Enter fullscreen mode Exit fullscreen mode

This approach improves readability, testability, and maintainability.

Lessons learned:

  • Follow the Single Responsibility Principle.
  • Break complex logic into smaller functions.
  • Reuse shared logic across Server Actions.

Conclusion

Next.js 14 Server Actions are a game-changer for modern web development, but they’re not foolproof. Junior developers often make mistakes like skipping validation, ignoring error boundaries, and overloading Server Actions with too much logic. By addressing these issues proactively—using validation libraries, handling errors gracefully, and adhering to the Single Responsibility Principle—you’ll build more robust, maintainable, and user-friendly applications.

Remember, Server Actions are a tool, and like any tool, they’re only as effective as how you use them. Apply these best practices, and you’ll avoid the common pitfalls that trip up many developers. Happy coding!


⚡ Want the Full Prompt Library?

I compiled all of these patterns (plus 40+ more) into the Senior React Developer AI Cookbook — $19, instant download. Covers Server Actions, hydration debugging, component architecture, and real production prompts.

Browse all developer tools at apolloagmanager.github.io/apollo-ai-store

Top comments (0)