Server-only Code in Next.js 15

Ensure your Next.js code is only executed on the server-side using these lesser known Next.js features

Next.js has revolutionized React development by introducing a powerful Server Components architecture within its App Router. This architecture brings new considerations for developers, particularly around code organization and execution context.

However, the boundaries between server and client code have become more blurred, and developers are often caught unaware by potential pitfalls concerning where the code is being executed. This can lead to pretty bad outcomes.

In this guide, we'll explore how to effectively manage the separation between server and client code in your Next.js applications, how to avoid leaking secrets and ensuring your code is only ever executed server-side.

The Foundation: Server vs Client Components

Before diving into the specifics of avoiding bundling server side code into the client, we need to understand the main culprits that lead to this problem.

With the introduction of Next.js App Router and React Server Components, we can now write React Components that are exclusively executed server-side. Here's the thing: data fetching also happens inside these components, which can lead to server-side data making its way to the client.

Data Fetching in Server Components

Let's take a look at an example of data fetching in a Server Component:

  1. A DashboardPage Server Component
  2. A Chart Client Component (eg. it uses use client, which means the component will both be rendered on the server and the client)
const apiSecret = process.env.API_SECRET;
async function DashboardPage() {
const data = await fetchData(apiSecret);
return <Chart data={data} />;
}

This server component fetches data from an API and renders a chart based on that data. It uses a secret key called API_SECRET to authenticate the request.

If we changes this slightly, we can see that the secret key is now leaked to the client as we mistakenly pass the apiSecret to the client component as a prop:

const apiSecret = process.env.API_SECRET;
async function DashboardPage() {
const data = await fetchData(apiSecret);
return <Chart data={{data, apiSecret}} />;
}

The apiSecret is now leaked to the client, which is not ideal. As uncommon a mistake as this may seem, you'd be surprised how many people make this mistake.

So, kinda bad, right? Let's take a look at how we can avoid this.

Better conventions for server-side code

The first practical step is to ensure that all server-side code is properly separated from the client.

In Makerkit, a SaaS Starter Kit for Next.js and Supabase, the convention is to always create a lib/server directory for server-side code, whether that's in a package or in the main app directory.

- components
- ... (all your components)
- lib
// Zod schemas, shared between server and client
- schema
// server-side code
- server
- auth.service.ts
// any shared code
- utils

This doesn't prevent you from accidentally importing server-side code into the client, but it does make it more explicit that you're aware of the boundaries between server and client code.

💡 When you write server-side code, always make sure to be explicit about where it's being executed.

Using the 'server-only' Package

The very best way to avoid accidentally importing server-side code into the client is to use the server-only package.

import 'server-only';
const apiSecret = process.env.API_SECRET;
export function fetchData() {
// sensitive operation, only executed on the server
const data = db.getData(apiSecret);
return data;
}

If you accidentally import a component using this file, you'll get a runtime error - and you'll know that you're doing something wrong.

💡 Add all server-side code into files that are marked with the server-only package.

Using the Taint API

The experimental Taint API is a new React API that allows you to mark values as tainted, which means they're not safe to pass to the client.

This is useful when you want to ensure that a value is only used on the server, and not on the client.

We can think of it asn additional layer of security, as it ensures that the value is only used on the server, and not on the client.

import {
experimental_taintUniqueValue as taintUniqueValue
} from 'react/experimental';
const apiSecret = taintUniqueValue(
'The API secret is only used on the server',
process,
process.env.API_SECRET,
);
export function fetchData() {
// sensitive operation, only executed on the server
const data = db.getData(apiSecret);
return data;
}

This alone is not enough to ensure that the value is only used on the server, since there are ways to bypass the taint API.

const key = taintUniqueValue(
'The API secret is only used on the server',
process,
process.env.API_SECRET,
);
const derivedKey = key.toUpperCase();

The constant key is now tainted, however, the derivedKey is not. This means that the value is still accessible on the client if it was accidentally passed to the client.

💡 Consider using the taint API to ensure that values are only used on the server, but do not solely rely on it to prevent leaking secrets.

Summarizing best practices

Here are the best practices we've discussed in this guide:

  1. Code Organization: Always ensure that all server-side code is properly separated from the client at the folder level.
  2. Place server code in their own files: Use the server-only package to ensure that server-side code is only ever executed on the server.
  3. Use the taint API to ensure that values are only used on the server, but do not solely rely on it to prevent leaking secrets.

While bundlers have gotten better at optimizing code, they're not perfect.

By following these best practices, you can build more secure, performant, and maintainable applications that leverage the full power of Next.js's Server Components.

Conclusion

In this guide, we've explored the importance of establishing clear boundaries between server and client code in Next.js applications. We've also discussed how to avoid leaking secrets and ensure that server-side code is only ever executed on the server.

By following these best practices, you can build more secure, performant, and maintainable applications that leverage the full power of Next.js's Server Components.