Thanks to Server Actions, developers are able to execute server-side code on user interaction, without having to create an API endpoint themselves. I’ve emphasised “themselves”, because server actions are a syntactic sugar for API endpoints, which to my surprise, is not that well-known amongst some professionals in the industry.
Even though the official Next.js documentation warns about this and the security around it, I couldn’t find an example online that unpacks Server Actions in detail. As a big advocate for learning by experimenting, I want this post to be a practical breakdown, which perhaps will engrave a better awareness than simply reading the docs.
Calling server actions outside of the app
Server actions are called using POST requests with the Next-Action
header containing the id of the action. The endpoint is actually the page where the action is invoked. We can find this out for ourselves pretty easily.
Consider a test scenario with:
- A simple Server Action in
src/actions/test.ts
, without an auth layer - A button to trigger it on the frontend
"use server";
export async function test() {
console.log(`Server action triggered`);
}
When you click the button, a new network request will appear in the Network
tab of your browser’s DevTools. Now, we can see that the Request URL
is indeed the path to the currently viewed page:
If the action is used in a different route, such as app/test/page.tsx
:
So, let’s grab the action id from the Next-Action
request header and send a POST request to the index page.
$ curl -X POST "https://my-app-wine-theta-86.vercel.app" \
-H "Content-Type: application/json" \
-H "Next-Action: 00bd5986a758215cec60b60be7ccb39f644a494adf" \
-d '[]'
# Vercel Logs output:
Server action triggered
The action executed the console.log
on the server, confirming the request was successful.
Passing arguments
You may have noticed the []
in the payload, which specifies the function’s arguments. Since the payload is JSON, Server Actions must use serializable arguments.
export async function echo(number: number, someText: string) {
console.log(`${number}: ${someText}`);
}
$ curl -X POST "https://my-app-wine-theta-86.vercel.app" \
-H "Content-Type: application/json" \
-H "Next-Action: 00bd5986a758215cec60b60be7ccb39f644a494adf" \
-d '[0, "hello from the outer world"]'
Finding the action id in the client bundle
It’s becoming clear why server actions should handle authorization within the function body.
But one may still argue, since the action id is non-deterministic and random, what if the user doesn’t get to run the action on the frontend (because component that calls it only renders after sign-in, for example)? It shouldn’t be exposed for others than the actual admins, right? Well, that’s only partially true.
We can define such component in a separate file, and just import it on the target page, conditionally rendering based on the auth status. If we go to the Sources
tab in DevTools, find the bundled chunk with the page (it starts with “page”), and search for createServerReference
we will find this:
let i = (0,
a.createServerReference)("001b7e7fe595e5c8a43b4161fa1f682be896041fbf", a.callServer, void 0, a.findSourceMapURL, "deleteAllUsers");
Not very sneaky, is it?
Of course, you could easily avoid this by simply using next/dynamic
and lazy loading the component, and it won’t be included in the initial JavaScript bundle. But this way, you’re at the mercy of your bundler and code splitting. It’s very error-prone as well, one import done wrong and the action will be exposed to the client.
Built-in security features
Next.js not only warns the developers about security (as I’ve mentioned in the introduction), but also implements some safety features. It’s important to be aware of them:
Secure action IDs: Next.js creates encrypted, non-deterministic IDs to allow the client to reference and call the Server Action. These IDs are periodically recalculated between builds for enhanced security.
Dead code elimination: Unused Server Actions (referenced by their IDs) are removed from client bundle to avoid public access by third-party.
Source: Next.js documentation
Conclusions
Server Actions are public API endpoints, and should be treated with the same precautions as traditional APIs are. There’s no better way to ensure security than being proactive and preventing accidents by properly adding an auth layer directly inside those functions.
Frameworks come with abstractions, and for every developer working on enterprise-grade/production-grade applications, it should be their responsibility to take time to properly understand them before using.