DEV Community

Cover image for Next Js: The missing part of how server actions are handling server-side logic
eyad akeleh
eyad akeleh

Posted on

Next Js: The missing part of how server actions are handling server-side logic

I wrote a 350-page book on Next.js. Here's my definitive guide to understanding how server actions handling server-side logic:

and see what's really happening under the hood when you, say, post an update on Facebook or finalize an order on Amazon.
Imagine you're building a massive online platform – maybe the next big social network or an e-commerce empire. You've got users doing all sorts of things:

  1. signing up,
  2. posting comments,
  3. buying products,
  4. adding items to carts. All these actions involve changing data, and that data lives in your "brain" – the server and its database.

1. Server Actions: Your Secret Agent for the Server
Think of Server Actions as highly efficient, super-secure digital agents. When you interact with a website, like clicking a "Submit" button on a form, you're usually sending data from your web browser (the "Client Component" or "Server Component" in the diagram) to the server.

1.1. The Old Way (Pre-Server Actions):

You'd often send data to an API endpoint (a special URL on your server). This involved writing separate API code, handling authentication, and then connecting it to your front-end. It worked, but it was like setting up a whole mini-mission for every small task.
1.2. The New Way (Server Actions):
Next.js 14 says, "Hey, why don't we make this simpler and safer?"
Now, you can write special JavaScript functions, right alongside your front-end code, that are marked to run exclusively on the server. That's what the 'use server'; directive is all about.

1.21.  Security:
Enter fullscreen mode Exit fullscreen mode

Your sensitive operations, like talking directly to the database to create a new user or process a payment, never leave the server. They're hidden away, protected from curious eyes or malicious attempts in the user's browser. It's like sending a highly trained, discreet operative to handle sensitive tasks in a secure facility, rather than giving a public broadcast.
1.22. Performance:
Because these actions run directly on the server, your user's browser doesn't have to do heavy lifting. This means faster response times and a smoother experience, which is crucial for big sites like Facebook where every millisecond counts.
1.23. Convenience:
We love this because it simplifies the code. You can keep related logic together, making your applications easier to build and maintain.

Here's a quick look at defining one, just as the article shows:
Enter fullscreen mode Exit fullscreen mode
 // app/actions.ts
        'use server'; // This magic line says: "Hey, this function runs on the server!"

    export async function createUser(formData: FormData) {
      // Imagine `db.users.create` is our secure vault manager
      // updating the central record.
      const user = await db.users.create({ data: formData });
      console.log(`User created on the server: ${user.username}`);
      return user;
    }
Enter fullscreen mode Exit fullscreen mode

Whether your form is in a "Client Component" (interactive stuff in the browser) or a "Server Component" (initial page build on the server), you can now trigger these createUser Server Actions securely and directly.

  1. Data Revalidation: Keeping Everyone Up-to-Date Now, let's talk about one of the trickiest parts of building dynamic web applications: “keeping data fresh” 2.1. The Stale Data Problem: Imagine you're on Amazon, looking at a product. Someone else just bought the last one. If your page is showing "5 in stock," that's stale data. Or on Facebook, you just posted a comment. If your friends' news feeds don't show it immediately, that's a bad experience. Websites use "caching" (storing copies of data closer to the user for faster access) to speed things up, but caches can get outdated. How do we tell the system, "Hey, this data just changed, everyone needs the new version!"?

2.22. Revalidation:

The Digital Refresh Button: This is where revalidation comes in. After a Server Action makes a change (like creating a user in our diagram), it needs to "revalidate" the cache. This tells Next.js to discard any old, cached versions of that data, so the next time someone requests it, they get the fresh, updated information directly from the source.

  1. Understanding the Revalidation Diagram

Let's walk through the diagram step-by-step, imagining our createUser Server Action is running after someone fills out a "Sign Up" form on our new social network.

3.1. The Initial Request:

Whether it's a Client Component (your interactive sign-up form in the browser) or a Server Component (a form that was rendered on the server), both can "Submit Form" data to our Server Action: createUser.
This is like you clicking "Create Account" or "Post Comment."

3.2. The Server Action Takes Over:
The form data is sent securely to our createUser Server Action running on the server.
First and foremost, this action will Process Data and update the Database. This is the single source of truth; if:

3.21 - a new user is created, it's recorded here.

3.22 - If a product is purchased, the stock count changes here.

  1. The Critical Branches: Revalidating the Cache
    After updating the database, the server action knows the data has changed. Now it needs to tell the rest of the application ecosystem to "forget" any old cached versions. This is where the diagram splits into two powerful revalidation methods:

    3.1. Revalidate Tag:
    Analogy:
    Imagine your data has "labels" or "tags" on it. For instance, all information about users might have the tag 'users'. When a new user is created, you tell the system: "Hey, anything you have cached with the tag 'users' is now potentially old. Get rid of it!"
    The diagram shows Tag Cache: 'Users', then Invalidate Cache. This means any cached data specifically tagged as 'users' will be marked as stale and refreshed on the next request.
    Code Example:
    Javascript

// Inside your 'createUser' Server Action:
            import { revalidateTag } from 'next/cache';

            export async function createUser(formData: FormData) {
              // ... database update logic ...
              revalidateTag('users'); // Tells Next.js to clear anything cached with the 'users' tag
              return user;
            }

Enter fullscreen mode Exit fullscreen mode

3.2. Revalidate Path:
Analogy:
Now, imagine specific "web addresses" or "pages" (paths) that display user information, like /users (a page listing all users) or /dashboard/profile (a user's profile page). When a user is created, you might say:
"Hey, the page at /users definitely needs to be refreshed because there's a new user!"
The diagram shows Path Cache: '/Users', then Invalidate Cache. This specifically tells Next.js to clear the cache for that particular URL path.
Code Example:
Javascript

            // Inside your 'createUser' Server Action:
            import { revalidatePath } from 'next/cache';

            export async function createUser(formData: FormData) {
              // ... database update logic ...
              revalidatePath('/users'); // Tells Next.js to clear the cache for the '/users' page
              return user;
            }
Enter fullscreen mode Exit fullscreen mode
  1. Displaying the Fresh Data:

After the cache is invalidated (either by tag or path), the diagram shows Update Client Component and Update Server Component, followed by Display Updated Data.
This means the next time that particular component (client or server) renders or fetches data, because the old cache is gone, Next.js will automatically go back to the source (your database, via fresh server-side logic) to get the latest information.
The user then sees the new user added, the updated product stock, or their fresh comment instantly.

  1. Best Practices for the Modern Software Engineer As we grow in career, understanding not just how things work, but how to use them well, is critical. Here are a few best practices inspired by Server Actions: 5.1. Be Surgical with Revalidation: When you revalidate, you're telling the system to do extra work (refetching data). Don't just revalidatePath('/') (refresh the whole website!) for a minor change. “Be precise” Use specific tags for groups of related data ('products', 'comments', 'orders') or specific paths (/product/[id], /user/[username]). This keeps your application performant and efficient, just like Amazon carefully updates only relevant product details when a price changes, not the entire catalog. 5.2. Embrace Optimistic Updates for User Experience: Sometimes, the database update might take a few milliseconds. For a better user experience, Next.js allows "optimistic updates." This means you immediately show the user that their action was successful (e.g., the "Like" button turns blue, the comment appears) before the server has even confirmed it. If the server action fails, you can then revert the UI. This makes apps feel incredibly fast and responsive, like what we experience on Facebook's instant "likes." Javascript
 // A simplified example using React's useOptimistic
    'use client';
    import { useOptimistic } from 'react';
    import { createUser } from './actions';

    export default function Page() {
      const [optimisticUsers, addOptimisticUser] = useOptimistic(
        [],
        (state, newUser) => [...state, newUser]
      );

      const handleSubmit = async (event) => {
        event.preventDefault();
        const formData = new FormData(event.target);
        const newUsername = formData.get('username');
        addOptimisticUser({ id: 'temp', username: newUsername }); // Show immediately!
        await createUser(formData); // Then send to server
      };

      return (
        // ... form and display optimisticUsers ...
      );
    }

5.3.  Always Handle Errors Gracefully:
Real-world systems fail. Databases go down, networks hiccup. Your Server Actions must include robust error handling (using `try...catch` blocks) to prevent your application from crashing and to provide meaningful feedback to the user. An e-commerce store like Amazon cannot afford to just show a blank page if a payment fails; it needs to tell the user what went wrong.
javascript

    'use server';
    export async function createUser(formData: FormData) {
      try {
        const user = await db.users.create({ data: formData });
        return { success: true, user };
      } catch (error) {
        console.error("Failed to create user:", error);
        return { success: false, error: "Failed to create user. Please try again." };
      }
    }
Enter fullscreen mode Exit fullscreen mode

5.4. Know When to Go Client-Side:
While Server Actions are powerful for data mutations, remember that client components are still your go-to for rich, interactive UI that doesn't need to touch the server – things like:

  1. complex animations,
  2. dragging and dropping,
  3. simple input validation before submission.

Server Actions handle the server-side logic, but the user's interactive playground is often still in the browser.
In essence, Server Actions in Next.js 14 are a huge step forward. They let us build incredibly dynamic, secure, and performant applications that feel snappy and reliable, just like the huge platforms we use every day. By understanding how they update data and efficiently tell the rest of the system to "refresh,". This became a fundamental skill for building the next generation of web experiences.

I cover this and much more in my new book for beginners. For the next 48 hours, it's only $29. Link in comments."

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.