Prevent sensitive data leaks by using the 'server-only' package, creating a protected Data Access Layer, filtering data into safe DTOs, and leveraging React Taint APIs for defense-in-depth
Preventing sensitive server-side data from leaking to the client is critical in Next.js applications, especially with the App Router where Server Components and Client Components coexist. The main risk occurs when server-only code (database queries, environment variables with secrets) is accidentally imported into Client Components or passed as props across the server-client boundary [citation:4]. Next.js provides multiple layers of protection: the 'server-only' package to enforce module boundaries, a Data Access Layer to centralize and sanitize data, Data Transfer Objects (DTOs) to expose only necessary fields, and experimental React Taint APIs for additional defense-in-depth [citation:1][citation:8].
Use the 'server-only' package to mark modules that should never be imported into client components, causing build errors if accidentally imported [citation:1][citation:7].
Create a dedicated Data Access Layer (DAL) that centralizes all data fetching and authorization logic on the server [citation:8].
Implement Data Transfer Objects (DTOs) that return only the specific fields needed for rendering, never full database objects [citation:1][citation:8].
Never pass entire data objects from Server Components to Client Components—always filter to only what's needed [citation:8].
Use environment variables correctly: never prefix secrets with NEXT_PUBLIC_, which embeds them in the client bundle [citation:1][citation:4].
Consider enabling experimental React Taint APIs to mark objects or values that should never cross the server-client boundary [citation:2][citation:8].
The most fundamental protection is the 'server-only' package, which causes a build error if a module is accidentally imported into client code [citation:1][citation:10]. This is especially important for modules that access environment variables, databases, or internal APIs. Install it with pnpm add server-only, then add import 'server-only' at the top of any server-only file. If a developer later imports this module into a Client Component, the build will fail with a clear error message, preventing the leak from reaching production [citation:1][citation:7].
The Next.js documentation recommends creating a dedicated Data Access Layer for new projects [citation:8]. This internal library controls how and when data is fetched, performs authorization checks, and returns safe, minimal Data Transfer Objects (DTOs) [citation:8]. The DAL should only run on the server and should never return full database objects. Instead, it projects only the fields needed for rendering. This centralizes all data access logic, making it easier to enforce consistent security and reducing the risk of authorization bugs [citation:8].
A common mistake is fetching a full database object in a Server Component and passing it directly to a Client Component as props [citation:8]. This exposes all fields—including sensitive ones like password hashes, internal notes, or API keys—to the client bundle. Even if you don't render them, they're still present in the serialized props [citation:8]. Always filter your data before passing it across the boundary. The Next.js documentation emphasizes that you should sanitize the data before passing it to the Client Component [citation:8].
Environment variables with the NEXT_PUBLIC_ prefix are inlined into the JavaScript bundle at build time and become visible to anyone using browser DevTools [citation:1][citation:4]. Never prefix secrets like database URLs, API keys, or authentication tokens with NEXT_PUBLIC_. Use standard variable names (e.g., DATABASE_URL) for server-only secrets, and only access them in Server Components, API routes, or the Data Access Layer [citation:1]. Create a .env.example file to document required variables without exposing real values [citation:1].
Next.js supports experimental React Taint APIs that provide an additional layer of defense [citation:2][citation:8]. You can enable them in next.config.js with experimental.taint: true. The APIs include experimental_taintObjectReference to prevent entire objects from crossing the server-client boundary, and experimental_taintUniqueValue to taint specific values like API keys [citation:2]. If a tainted object or value is passed to a Client Component, React throws an error. However, the documentation warns not to rely on tainting as your only mechanism—it's a defensive addition, not a replacement for proper data filtering [citation:2][citation:8].
Use folder conventions: Place all server-only code in a lib/server directory to make boundaries explicit [citation:7].
Never trust client input: Always validate and sanitize data from forms, URL parameters, and headers—even in Server Actions [citation:8].
Use parameterized queries: Prevent SQL injection by using database APIs that support safe templating [citation:8].
Review bundle size: Use @next/bundle-analyzer to periodically check what's included in your client bundles [citation:3][citation:6].
Implement authorization checks: Always verify permissions in the Data Access Layer, not just in middleware [citation:4][citation:8].