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 });
}
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' };
}
}
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;
}
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' };
}
}
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>
);
}
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 });
}
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.' };
}
}
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;
}
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;
}
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');
});
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)