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

Server Actions in Next.js 14 are a game-changer for handling form submissions, API interactions, and server-side logic without the need for traditional API routes. However, as someone who’s spent countless hours debugging and mentoring junior developers, I’ve noticed recurring patterns of mistakes when implementing Server Actions. These errors often stem from misunderstandings about validation, error handling, and architectural best practices. In this article, I’ll break down these common pitfalls, provide corrected examples, and share lessons I’ve learned from real-world scenarios.


1. Not Validating Input Before Server Actions

One of the most frequent mistakes junior developers make is assuming that the data sent to a Server Action will always be valid. This is a dangerous assumption, as malicious users or misconfigured clients can send unexpected data.

The Mistake: Lack of Input Validation

Here’s an example of a Server Action without validation:

async function handleSubmit(formData) {
  const email = formData.get('email');
  const password = formData.get('password');
  await loginUser(email, password);
}
Enter fullscreen mode Exit fullscreen mode

If email or password is missing or invalid, the Server Action will fail silently or expose sensitive errors.

The Fix: Use Zod for Schema Validation

To fix this, I always recommend using a validation library like Zod. Here’s how you can validate input before proceeding:

import { z } from 'zod';

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

async function handleSubmit(formData) {
  const rawData = Object.fromEntries(formData.entries());
  const result = loginSchema.safeParse(rawData);

  if (!result.success) {
    return { error: 'Invalid input' };
  }

  const { email, password } = result.data;
  await loginUser(email, password);
}
Enter fullscreen mode Exit fullscreen mode

By parsing the input with Zod, you ensure that the data is valid before passing it to your business logic. This reduces the risk of errors and improves security.


2. Not Handling Errors Gracefully

Another common mistake is failing to handle errors properly in Server Actions. Without proper error handling, users might see cryptic messages or, worse, no feedback at all.

The Mistake: Swallowing Errors

Consider this implementation:

async function handleSubmit(formData) {
  try {
    const email = formData.get('email');
    const password = formData.get('password');
    await loginUser(email, password);
  } catch (error) {
    console.error(error);
  }
}
Enter fullscreen mode Exit fullscreen mode

While the error is logged, the user is left in the dark about what went wrong.

The Fix: Return User-Friendly Messages

To improve this, return user-friendly error messages:

async function handleSubmit(formData) {
  try {
    const email = formData.get('email');
    const password = formData.get('password');
    await loginUser(email, password);
  } catch (error) {
    return { error: 'Login failed. Please check your credentials and try again.' };
  }
}
Enter fullscreen mode Exit fullscreen mode

You can also differentiate between error types for better UX:

catch (error) {
  if (error.message.includes('Invalid credentials')) {
    return { error: 'Invalid email or password.' };
  }
  return { error: 'Something went wrong. Please try again later.' };
}
Enter fullscreen mode Exit fullscreen mode

3. Not Leveraging Error Boundaries

Server Actions often interact with external APIs or databases, which can fail unpredictably. Junior developers sometimes forget to implement proper error boundaries, leading to crashes.

The Mistake: No Error Boundary

Here’s an example that lacks an error boundary:

async function fetchUserData(userId) {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const data = await response.json();
  return data;
}
Enter fullscreen mode Exit fullscreen mode

If the API fails, the entire page might crash.

The Fix: Use Error Boundaries

Next.js provides built-in support for error boundaries. Wrap your components in an error boundary to handle crashes gracefully:

'use client';

import { ErrorBoundary } from 'react-error-boundary';

function UserProfile({ userId }) {
  return (
    <ErrorBoundary fallback={<div>Failed to load user data.</div>}>
      <FetchUserData userId={userId} />
    </ErrorBoundary>
  );
}

async function FetchUserData({ userId }) {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  if (!response.ok) throw new Error('Failed to fetch user data');
  const data = await response.json();
  return <div>{data.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

This ensures that even if the Server Action fails, the user sees a fallback UI instead of a broken page.


4. Overloading Server Actions with Too Much Logic

Junior developers often fall into the trap of cramming too much logic into a single Server Action, making it hard to debug and maintain.

The Mistake: Monolithic Server Action

Here’s an example of a Server Action that does too much:

async function handleOrder(formData) {
  const items = JSON.parse(formData.get('items'));
  const total = calculateTotal(items);
  const user = await getUser();
  const order = await createOrder(user.id, items, total);
  await sendConfirmationEmail(order.id);
}
Enter fullscreen mode Exit fullscreen mode

If any step fails, it becomes hard to isolate the issue.

The Fix: Break Down Logic into Smaller Functions

Refactor the logic into smaller, reusable functions:

async function calculateOrderTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

async function createUserOrder(userId, items, total) {
  return await prisma.order.create({ data: { userId, items, total } });
}

async function handleOrder(formData) {
  const items = JSON.parse(formData.get('items'));
  const total = await calculateOrderTotal(items);
  const user = await getUser();
  const order = await createUserOrder(user.id, items, total);
  await sendConfirmationEmail(order.id);
}
Enter fullscreen mode Exit fullscreen mode

This makes the code easier to test and debug.


Lessons Learned

  1. Validate early and often. Use libraries like Zod to ensure data integrity.
  2. Handle errors gracefully. Provide clear, user-friendly messages and log errors for debugging.
  3. Use error boundaries. Prevent crashes by wrapping components in error boundaries.
  4. Keep Server Actions focused. Break down complex logic into smaller, reusable functions.

Conclusion

Server Actions in Next.js 14 are a powerful tool, but they require careful implementation to avoid common pitfalls. By validating input, handling errors gracefully, leveraging error boundaries, and keeping logic modular, you can build robust and maintainable applications. As a junior developer, mastering these patterns will set you apart and save you countless hours of debugging. 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)