Пошук уроків, статей та іншого контенту
Дізнаєтеся, як ізолювати серверні модулі, секрети й доступ до бази від клієнтського бандла.
У Next.js з App Router компоненти за замовчуванням є серверними. Вони виконуються на сервері, тому можуть:
читати змінні середовища без префікса NEXT_PUBLIC_;
звертатися до бази даних;
викликати внутрішні API;
працювати з файловою системою;
використовувати серверні секрети.
Компонент із директивою 'use client' потрапляє до клієнтського бандла. Такий компонент виконується в браузері, тому серверні модулі не повинні бути доступними через його імпорти.
Важливе правило:
Якщо клієнтський компонент імпортує модуль, цей модуль і його залежності можуть стати частиною клієнтського графа модулів.
Тому недостатньо назвати файл db.server.ts. Next.js не захищає модуль лише за назвою файлу.
server-onlyПакет server-only дозволяє явно позначити модуль як серверний.
Встановіть його в проєкті:
npm install server-onlyНа початку модуля, який не можна імпортувати на клієнті, додайте:
import 'server-only';Якщо такий модуль прямо або опосередковано імпортувати з Client Component, Next.js повідомить про помилку під час збирання. Це захищає код від випадкового потрапляння до браузерного бандла.
Розглянемо структуру:
app/
page.tsx
users/
UserList.tsx
lib/
users.ts
types/
user.tsСпочатку створимо спільний тип. У ньому немає серверних імпортів, тому його можна використовувати і на сервері, і на клієнті.
types/user.tsexport type User = {
id: number;
name: string;
};Тепер створимо модуль доступу до бази даних.
lib/users.tsimport 'server-only';
import { Pool } from 'pg';
import type { User } from '@/types/user';
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error('Змінна DATABASE_URL не налаштована');
}
const pool = new Pool({
connectionString: databaseUrl,
});
export async function getUsers(): Promise<User[]> {
const result = await pool.query<User>(`
SELECT id, name
FROM users
ORDER BY name
`);
return result.rows;
}У цьому модулі:
import 'server-only' забороняє імпорт із клієнтського коду;
DATABASE_URL читається тільки на сервері;
підключення до бази даних не потрапляє до браузера;
назви користувачів ідентифікуються окремим типом без серверних залежностей.
Для цього прикладу потрібен пакет pg:
npm install pg
npm install --save-dev @types/pgЗмінна середовища може бути задана у файлі .env.local:
DATABASE_URL=postgresql://app_user:password@localhost:5432/app_dbНе додавайте префікс NEXT_PUBLIC_ до секретних змінних. Змінна NEXT_PUBLIC_DATABASE_URL призначалася б для використання в браузері.
Файл app/page.tsx є Server Component, оскільки не містить 'use client'.
app/page.tsximport { getUsers } from '@/lib/users';
import { UserList } from '@/app/users/UserList';
export default async function HomePage() {
const users = await getUsers();
return (
<main>
<h1>Користувачі</h1>
<UserList users={users} />
</main>
);
}Серверний компонент може напряму викликати getUsers. Запит до бази виконується на сервері під час формування сторінки.
До клієнтського компонента передається лише результат запиту:
ідентифікатор користувача;
ім’я користувача.
Підключення до бази, пароль і DATABASE_URL не передаються через props.
Інтерактивність потрібна, наприклад, для фільтрації списку. Для цього список можна зробити клієнтським компонентом.
app/users/UserList.tsx'use client';
import { useState } from 'react';
import type { User } from '@/types/user';
type UserListProps = {
users: User[];
};
export function UserList({ users }: UserListProps) {
const [query, setQuery] = useState('');
const visibleUsers = users.filter((user) =>
user.name.toLowerCase().includes(query.toLowerCase()),
);
return (
<section>
<label>
Пошук:
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
</label>
<ul>
{visibleUsers.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</section>
);
}Цей компонент правильно ізольований:
він імпортує тільки тип User;
він не імпортує getUsers;
він не імпортує pg;
він не має доступу до DATABASE_URL;
у клієнтський бандл потрапляє лише код фільтрації списку.
Не слід робити так:
'use client';
import { getUsers } from '@/lib/users';
export function UserList() {
// Помилка: серверний модуль імпортується клієнтським компонентом
return null;
}Завдяки server-only Next.js зупинить збирання та вкаже на неправильну межу.
Змінні без префікса NEXT_PUBLIC_ призначені для серверного коду:
const databaseUrl = process.env.DATABASE_URL;
const apiToken = process.env.INTERNAL_API_TOKEN;Їх можна використовувати в:
Server Components;
Route Handlers;
Server Actions;
серверних модулях, які імпортуються тільки серверним кодом.
Змінні з префіксом NEXT_PUBLIC_ можуть бути вбудовані в JavaScript-код клієнта:
NEXT_PUBLIC_API_URL=https://example.comТому не використовуйте цей префікс для:
паролів;
токенів;
ключів приватних API;
рядків підключення до бази;
приватних URL внутрішніх сервісів.
Навіть якщо змінна не має NEXT_PUBLIC_, не потрібно передавати її значення у props або повертати з API-відповіді.
Небезпечно:
export default function Page() {
return <pre>{process.env.INTERNAL_API_TOKEN}</pre>;
}Так секрет стане частиною HTML-відповіді та буде доступний користувачу.
Серверний код може потрапити до клієнтського графа не лише через прямий імпорт.
Наприклад, є модулі:
components/Search.tsx
lib/catalog.ts
lib/db.tsЯкщо Search.tsx — Client Component і він імпортує catalog.ts, а catalog.ts імпортує db.ts, то серверний код опинився в клієнтському ланцюжку:
Search.tsx → catalog.ts → db.tsТому server-only потрібно додавати до кожного модуля, який містить серверні залежності або секрети:
// lib/db.ts
import 'server-only';// lib/catalog.ts
import 'server-only';
import { getProductsFromDatabase } from './db';
export async function getCatalog() {
return getProductsFromDatabase();
}Клієнтський компонент не повинен імпортувати ні db.ts, ні catalog.ts. Він має отримати вже підготовлені безпечні дані від Server Component або через серверний endpoint.
Server Component не надсилається до браузера як звичайний JavaScript-код для виконання. Сервер виконує його і формує результат для клієнта.
Це дає змогу тримати на сервері:
import 'server-only';
import { readFile } from 'node:fs/promises';
export async function readPrivateConfiguration() {
return readFile('/var/app/private-config.json', 'utf8');
}Але якщо компоненту потрібні браузерні API або стан React, його доводиться зробити Client Component. У такому разі серверні операції слід залишити в окремому серверному модулі, а клієнту передавати тільки результат.
Розділяйте відповідальність:
серверний модуль отримує дані;
Server Component викликає серверний модуль;
Client Component відповідає за взаємодію та відображення;
між ними передаються лише дані, які можна показати користувачу.
Під час розробки перевіряйте:
Чи є в модулі з базою або секретами import 'server-only'.
Чи імпортує Client Component серверний модуль прямо або через інший модуль.
Чи не має секретна змінна префікса NEXT_PUBLIC_.
Чи не передається секрет через props.
Чи не повертається секрет у JSON-відповіді.
Чи не виводиться секрет у помилку або HTML.
Чи містить клієнтський код лише типи й дані, потрібні інтерфейсу.
Назва database.server.ts сама по собі не створює захисту.
Краще явно додати:
import 'server-only';Навіть якщо клієнтський компонент викликає функцію лише після натискання кнопки, сам імпорт уже порушує межу.
Серверний код потрібно викликати через серверний механізм Next.js, а не імпортувати в браузерний компонент.
Небезпечно повертати об’єкт із зайвими полями:
return {
users,
databaseUrl: process.env.DATABASE_URL,
};Формуйте результат із мінімальним набором даних, потрібним інтерфейсу:
return users.map((user) => ({
id: user.id,
name: user.name,
}));NEXT_PUBLIC_ для «зручного доступу»Цей префікс не робить змінну безпечнішою. Навпаки, він означає, що значення дозволено використовувати в клієнтському коді.
Модуль із серверним клієнтом бази даних, токенами або node:-залежностями не повинен бути спільним модулем для сервера й браузера. Типи та чисті функції можна винести в окремі файли без серверних імпортів.
Server Components виконуються на сервері та можуть працювати із секретами й базою даних.
Client Components потрапляють до браузерного бандла, тому не повинні імпортувати серверні модулі.
import 'server-only' явно позначає модуль як доступний лише на сервері.
Змінні без NEXT_PUBLIC_ потрібно використовувати тільки в серверному коді.
До клієнта передавайте лише безпечні дані, необхідні для інтерфейсу.
Захищайте не тільки прямі, а й транзитивні імпорти серверних модулів.
Назва файлу не замінює явної перевірки server-only.