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 powerful feature that allows developers to execute server-side logic directly from client-side components. However, as I’ve mentored junior developers, I’ve noticed recurring patterns of mistakes that can lead to bugs, security vulnerabilities, and poor user experiences. In this article, I’ll dive into the most common pitfalls, how to avoid them, and provide practical examples to help you get it right.


Mistake 1: Not Validating Inputs Properly

One of the most frequent mistakes I see is assuming that client-side validation is enough. While client-side validation improves user experience, it’s trivial to bypass. Server Actions must validate every input to ensure data integrity and security.

Example: Missing Server-Side Validation

Imagine a form where users submit their email address. A junior developer might write a Server Action like this:

async function submitEmail(formData) {
  const email = formData.get('email');
  await db.user.create({ email });
}
Enter fullscreen mode Exit fullscreen mode

This code blindly trusts the input, which is dangerous. An attacker could submit malicious data or invalid formats.

Solution: Use Zod for Validation

Zod is a fantastic library for schema validation. Here’s how to fix the above example:

import { z } from 'zod';

const emailSchema = z.string().email();

async function submitEmail(formData) {
  const email = formData.get('email');
  try {
    const validatedEmail = emailSchema.parse(email);
    await db.user.create({ email: validatedEmail });
  } catch (error) {
    return { error: 'Invalid email address' };
  }
}
Enter fullscreen mode Exit fullscreen mode

By using Zod, we ensure that only valid email addresses are processed.


Mistake 2: Ignoring Error Boundaries

Junior developers often forget to handle errors gracefully in Server Actions. When something goes wrong, users are left staring at a broken page or unclear error messages.

Example: No Error Handling

Consider this Server Action that fetches user data:

async function getUserData(userId) {
  const user = await db.user.findUnique({ where: { id: userId } });
  return user;
}
Enter fullscreen mode Exit fullscreen mode

If userId is invalid or the database is down, this action throws an unhandled error, crashing the application.

Solution: Use Try-Catch and Error Boundaries

Wrap your Server Actions in try-catch blocks and provide meaningful error messages:

async function getUserData(userId) {
  try {
    const user = await db.user.findUnique({ where: { id: userId } });
    if (!user) throw new Error('User not found');
    return user;
  } catch (error) {
    console.error('Error fetching user data:', error);
    return { error: 'Failed to fetch user data' };
  }
}
Enter fullscreen mode Exit fullscreen mode

Additionally, use Next.js error boundaries to gracefully handle errors in your UI:

'use client';

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

function UserProfile({ userId }) {
  return (
    <ErrorBoundary fallback={<div>Something went wrong</div>}>
      <UserDetails userId={userId} />
    </ErrorBoundary>
  );
}
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Not Securing Server Actions

Server Actions are executed on the server, but junior developers sometimes forget that they can still be exploited if not secured properly. For example, an attacker could manipulate the request body or repeatedly trigger actions.

Example: Lack of Rate Limiting

Imagine a Server Action for submitting a comment:

async function submitComment(formData) {
  const comment = formData.get('comment');
  await db.comment.create({ content: comment });
}
Enter fullscreen mode Exit fullscreen mode

Without rate limiting, this action could be abused to spam the database.

Solution: Implement Rate Limiting

Use a library like rate-limiter-flexible to enforce rate limits:

import { RateLimiterMemory } from 'rate-limiter-flexible';

const rateLimiter = new RateLimiterMemory({
  points: 5, // 5 requests
  duration: 60, // per 60 seconds
});

async function submitComment(formData) {
  const comment = formData.get('comment');
  try {
    await rateLimiter.consume('comment_submit');
    await db.comment.create({ content: comment });
  } catch (error) {
    return { error: 'Too many requests. Please try again later.' };
  }
}
Enter fullscreen mode Exit fullscreen mode

Mistake 4: Overfetching or Underfetching Data

Junior developers often struggle with optimizing data fetching in Server Actions. Overfetching can lead to performance issues, while underfetching results in incomplete data.

Example: Overfetching Data

This Server Action fetches an entire user object when only the name is needed:

async function getUserName(userId) {
  const user = await db.user.findUnique({ where: { id: userId } });
  return user.name;
}
Enter fullscreen mode Exit fullscreen mode

Solution: Fetch Only What You Need

Optimize the query to fetch only the required fields:

async function getUserName(userId) {
  const user = await db.user.findUnique({
    where: { id: userId },
    select: { name: true },
  });
  return user.name;
}
Enter fullscreen mode Exit fullscreen mode

Mistake 5: Not Testing Server Actions

Testing is often overlooked, especially by junior developers. Server Actions must be tested to ensure they behave as expected under various conditions.

Solution: Write Unit Tests

Use Jest or another testing framework to test your Server Actions:

import { submitEmail } from './actions';

test('submitEmail rejects invalid email', async () => {
  const formData = new FormData();
  formData.append('email', 'invalid-email');
  const result = await submitEmail(formData);
  expect(result.error).toBe('Invalid email address');
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

Server Actions in Next.js 14 are a game-changer, but they require careful handling to avoid common pitfalls. By validating inputs, handling errors, securing actions, optimizing data fetching, and writing tests, you can build robust and reliable applications. As you grow as a developer, these patterns will become second nature. Keep practicing, and don’t hesitate to ask for feedback or review your code critically. 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)