DEV Community

Cover image for What are taints and server actions in NextJS 14?
Afan Khan
Afan Khan

Posted on • Originally published at Medium

What are taints and server actions in NextJS 14?

Introduction

NextJS and its developers are prioritizing security. It’s time to understand the importance of data and the consequences. Most people are worried whether NextJS 14 will be problematic to implement.

Many recently migrated to the 13th version that introduced the App Router, and people are worried whether this new update will cause them to switch again. However, experienced developers are stress-free.

This release did not excite them. It did not prompt them to jump out of their chairs. The updates remained insignificant for some, and the lack of new APIs exhausted people.

But that doesn’t indicate whether the release was utterly worthless. We have a few amusing features, and I will cover two of them in this article.

Let’s understand Server Actions and Taints. I found people talking about them and insulting Server Actions, originally on Twitter. Later, I heard Theo talk about them in his videos referring to the NextJS Conf.

So, naturally, I started to get into the rabbit hole of this new update, and here’s what I found.

Server Actions

Server actions represent a function that allows you to write server-related code inside of code that looks like client code. Wait, WTF does that mean? It represents a set of actions inside a function that will run on the server but in a syntax that seems like you are writing client-side React components.

Server actions are called straight from your React components. Vercel describes it as a function that runs securely on the server directly from your React components to avoid writing manual API routes or route handlers.

NextJS is trying to add more security features to your codebases and applications. While server actions allow you to write code that will execute on the server, you can not put it inside client components because they avoid sending data to client devices while improving the developer experience. All of that is for security.

You’re solely writing code that will execute on the server, like fetching data from a database, in a client-side fashion for simplicity and security reasons.

export default function Page() {
  async function create(formData: FormData) {
    'use server';
    const id = await createItem(formData);
  }

  return (
    <form action={create}>
      <input type="text" name="name" />
      <button type="submit">Submit</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Take this code example from the documentation. For now, this code is secure because the JavaScript remains strictly on the server as we are using server actions.

However, this code will breach the policies of server actions and the entire security tantrum if the code in the return statement remains visible to the user since it incorporates the information stored in a function outside that statement.

Server Actions hide the information of those outside functions and only make the HTML visible to the client.

The code snippet is a server component, and except for the <form> HTML element, which will get sent to the client’s device, the entire JavaScript in the file remains in an imaginary vault and avoids exposing itself to the client.

When the user clicks submit, the application pings a post request to the mentioned endpoint using ordinary web standards instead of AJAX, fetch, or anything remotely similar.

Most people confuse back-end-related server action functions for APIs with the code inside the return statement defining the static front-end HTML markup because it is a JSX front-end file. However, JSX files and components are not client-side oriented anymore.

It is something that the user will see, but the JavaScript will not get sent to the user. By the way, this is not a new concept either.

Server actions have been around for quite a while now, and I’ve used them multiple times in my applications, but they are finally stable. There’s a significant difference in whether a code should get into production based on the status of the features and whether they are stable.

Vercel deeply integrated Server actions with the App Router, which will help people using NextJS 13. You can redirect to different routes using the redirect() method, set and read cookies using the cookies() method, etc.

Taints

As I stated earlier, Vercel is prioritizing security. The name “Taint” is a ploy to attract more people towards this new concept that promotes security. However, simply put, it is an experimental feature in the form of an API that allows specific objects to avoid getting pushed to the client side.

The rendering process of React begins with a plain HTML file with only a script tag. Then, it re-renders the empty HTML page and loads all the components with actual data, which is the process of hydration.

During this loading phase, attributes get inserted into HTML elements dynamically. Some of these attributes are confidential. React and Next insert fields or private attributes inside HTML elements to identify which actions to execute and what context or data is required to run those actions.

These actions could be fetching data from another source in the same application, getting data from a database, or working with an API. We don’t want our tokens to appear on HTML elements for server actions to operate appropriately. We want them hidden. We want them to be private. I don’t want my account to get hacked this easily. Therefore, taint exists.

The basic idea is to avoid large objects accidentally getting exposed to the client. You can enable taint through the next.config.js file.


module.exports = {
  experimental: {
    taint: true,
  },
};
Enter fullscreen mode Exit fullscreen mode

Now, you can mark objects that should not get passed to the client side using the experimental_taintObjectReference(message, object) method.

import { experimental_taintObjectReference } from 'react';

export async function getUserData(id) {
  const data = ...;
  experimental_taintObjectReference(
    'Do not pass user data to the client',
    data
  );
  return data;
}
Enter fullscreen mode Exit fullscreen mode

The taint feature allows the returned values to remain secure. If trying to make files explicitly limited and only accessible on the server, not the client, we can use import 'server-only'; on the top of those files. It will not allow anyone to import those files and export its modules.

If you try to import the getUserData() function from the above code snippet into another file, you cannot use the object directly due to security concerns. The following code snippet will fail to execute.

import { getUserData } from './data';

export async function Page({ searchParams }) {
  const userData = getUserData(searchParams.id);
  return <ClientComponent user={userData} />;
}
Enter fullscreen mode Exit fullscreen mode

However, developers can still access and extract data fields from the object and pass them along.

export async function Page({ searchParams }) {
  const { name, phone } = getUserData(searchParams.id);
  // Intentionally exposing personal data
  return <ClientComponent name={name} phoneNumber={phone} />;
}
Enter fullscreen mode Exit fullscreen mode

Intentially, we can expose the personal data to the client side by extracting those data fields and destructuring the object.

The above code snippet will only extract the specific required data, like the name and phone number, while the rest remains untouched and sacred in an imaginary NextJS vault.

Not only objects, but specific values, such as tokens, can also remain hidden from the client device using the taintUniqueValue(errorMessage, object, value) call with an additional parameter. It is the value in strings, numbers, etc., after the object argument. However, taint cannot block derived values.

import { experimental_taintObjectReference, experimental_taintUniqueValue } from 'react';

export async function getUserData(id) {
  const data = ...;
  experimental_taintObjectReference(
    'Do not pass user data to the client',
    data
  );
  experimental_taintUniqueValue(
    'Do not pass tokens to the client',
    data,
    data.token
  );
  return data;
}
Enter fullscreen mode Exit fullscreen mode

Taints can still get messy, and people can go over it. Instead of relying on them, you can combine taints as an additional layer of protection on top of something like a Data Access Layer.

What Else?

Don't get stressed about migrating your applications from NextJS 13 to 14. Most people will not use these new features, except the stable server actions, because we have been using them unknowingly for the longest time.

It is simple to migrate and not necessary. The rest is on you. I will cover partial pre-rendering in a separate article. It is another new technique introduced in NextJS 14. They also spoke more about the NextJS Compiler with Turboback. It is their rust-based bundler for JS and TS.

However, the compiler is unstable because it is not passing all the tests, so we will let developers smash their heads against it for a while.

Nevertheless, Vercel also released React Learn, a course in the form of a book, trying to educate people about NextJS 13 and the basics of React to allow them to migrate from other languages effortlessly.

Besides, uneducated people are also hating server actions for apparently reasons that arise from their lack of knowledge. In the NextJS Conf, the presenter displayed a slide explaining server actions with an open SQL statement using external values, like a slug, to use the insert statement for an SQL DB.

However, the presenter used a specific in-built feature of TypeScript, which they did not cite in the slide, and that feature allows functions to be called using template literals and avoid the rest of the string. For more details on that feature, watch this.

Until then.

Top comments (0)