---
title: "Лучшие практики"
description: "Паттерны разработки, специфичные для Ring Platform — серверные действия, доступ к базе данных, аутентификация, платежи и тестирование."
locale: "ru"
---
# Лучшие практики

Паттерны разработки и конвенции, специфичные для Ring Platform.

### For founders

## Принципы разработки

### Качество превыше количества
Ring Platform — это мультитенантная white-label платформа, обеспечивающая работу более 15 продакшен-клонов. Каждый паттерн должен одинаково работать как в средах с PostgreSQL (`k8s-postgres-fcm`), так и с Firebase. Уровень абстракции `DatabaseService` гарантирует это — пишите один раз в `db()`, работайте везде.

### Безопасность по конвенции
- `userId` **всегда** получается из `await auth()` в серверном коде, никогда из параметров, предоставленных клиентом.
- Платежные операции выполняются только на сервере. HMAC-подписи WayForPay верифицируют подлинность обратных вызовов.
- Загрузка файлов проходит через абстракцию `file()` (`@/lib/file`) — контроль доступа применяется на уровне загрузки, никогда не предполагается со стороны клиента.

### i18n не является опциональным
Все строки, ориентированные на пользователя, находятся в JSON-пакетах `locales/{locale}/`, загружаемых через `next-intl`. Строки непосредственно в компонентах являются признаком плохого кода. См. `docs/en/architecture/proxy-and-intl.mdx` для архитектуры i18n.

### Уважайте режим бэкенда
`DB_BACKEND_MODE` выбирает адаптер базы данных во время выполнения. Не импортируйте `firebase-admin` или `postgres` напрямую в доменном коде. Используйте исключительно `db()` из `@/lib/database`. См. [Режимы бэкенда и базы данных](/en/docs/architecture/backend-modes-and-databases).

## Доступ к базе данных

### Синглтон `db()`

Основной интерфейс базы данных — это синглтон `db()` из `@/lib/database`. Он автоматически инициализируется и предоставляет типизированные методы `*Doc`, возвращающие объекты результата `{ success, data, error }`.

```typescript
import { db } from '@/lib/database'

// Создание
const result = await db().createDoc('entities', { name: 'Acme', type: 'technology' })
if (!result.success) throw result.error

// Чтение по ID
const entity = await db().findDocById('entities', id)
if (!entity.success) throw entity.error

// Запрос с фильтрами
const results = await db().queryDocs({
  collection: 'entities',
  filters: [{ field: 'status', operator: '==', value: 'active' }],
  orderBy: [{ field: 'name', direction: 'asc' }],
  pagination: { limit: 20, offset: 0 },
})

// Обновление
await db().updateDoc('entities', id, { name: 'Acme Corp' })

// Удаление
await db().deleteDoc('entities', id)
```

> **Info**
> Синглтон `db()` переживает HMR Next.js благодаря `globalThis.__ringDatabaseService`. База данных инициализируется один раз и использует соединение повторно во время горячих перезагрузок.

### Транзакции

Используйте `db().transaction()` для атомарных многошаговых записей. Обратный вызов транзакции получает `IDatabaseTransaction` с методами `create`, `read`, `update`, `delete`. Ошибки **выбрасываются** — не проверяйте `result.success` внутри транзакции.

```typescript
import { db } from '@/lib/database'

await db().transaction(async (txn) => {
  const existing = await txn.read('usernames', usernameKey)
  if (existing) throw new Error('Username is taken')

  await txn.create('usernames', { userId, reservedAt: new Date() }, { id: usernameKey })
  await txn.update('users', userId, { username })
})
```

### Прямой доступ к сервису (продвинутый)

Для специализированной маршрутизации бэкенда или операций с несколькими адаптерами импортируйте класс `DatabaseService` напрямую:

```typescript
import { getDatabaseService } from '@/lib/database/DatabaseService'
const svc = getDatabaseService()
await svc.initialize()
```

См. [Режимы бэкенда и базы данных](/en/docs/architecture/backend-modes-and-databases) для деталей по выбору адаптера и синхронизации.

> **Warning**
> **Не** используйте сырые запросы Firestore или pg в коде функций. Каждая операция с базой данных должна проходить через `DatabaseService`, чтобы платформа могла менять бэкенды без переписывания бизнес-логики.

## Серверные действия

Все серверные действия находятся в `app/_actions/` и следуют согласованному паттерну.

### Конвенции

1. Директива `'use server'` на уровне файла в начале.
2. `import { auth } from '@/auth'` для сессии — никогда не принимайте `userId` от клиента.
3. Возвращайте объекты состояния формы `{ success?: boolean, error?: string, message?: string }`.
4. Используйте `localizedRedirect` для навигации после успеха.
5. Валидация через Zod или вручную на границе.

```typescript
'use server'

import { auth } from '@/auth'
import { db } from '@/lib/database'
import { revalidatePath } from 'next/cache'
import { localizedRedirect } from '@/lib/i18n-server-redirect'
import type { Locale } from '@/i18n/shared'

export interface ActionResult {
  success?: boolean
  error?: string
  message?: string
  fieldErrors?: Record<string, string>
}

export async function createEntity(
  prevState: ActionResult | null,
  formData: FormData,
  locale: Locale
): Promise {
  const session = await auth()
  if (!session?.user?.id) return { error: 'Authentication required' }

  // Валидация
  const name = formData.get('name') as string
  if (!name?.trim()) return { error: 'Name is required' }

  // Запись
  const result = await db().createDoc('entities', {
    name: name.trim(),
    createdBy: session.user.id,
    createdAt: new Date(),
  })

  if (!result.success) return { error: result.error?.message ?? 'Creation failed' }

  revalidatePath(`/${locale}/entities`)
  localizedRedirect({ locale, href: '/entities' })
}
```

### Обработка ошибок

- Возвращайте строки ошибок для отображения в форме. Не выбрасывайте ошибки в действиях, возвращающих формы.
- Выбрасывайте только для невосстановимых ошибок (неправильная конфигурация, отсутствующие переменные окружения).
- Оборачивайте `localizedRedirect` в try/catch — он выбрасывает `NEXT_REDIRECT`, который должен распространяться.

> **Tip**
> Проверьте `app/_actions/vendor-actions.ts` для комплексного реального примера, охватывающего загрузку файлов (Vercel Blob через `file()`), вложенное создание + обновление и многошаговые потоки онбординга.

## Аутентификация

### Серверная сторона (`@/auth`)

Импортируйте `auth` из корневого `@/auth` (не напрямую из `next-auth` или `authjs` — Ring Platform предоставляет настроенный экземпляр в `auth.ts`).

```typescript
import { auth } from '@/auth'

export async function getServerSession() {
  const session = await auth()
  return session
}
```

Вызов `auth()` доступен в:
- Серверных действиях (`app/_actions/`)
- Обработчиках маршрутов (`app/api/`)
- Серверных компонентах (асинхронный компонент)
- Промежуточном ПО (middleware)

### Клиентская сторона (`useSession`)

```typescript
'use client'

import { useSession } from '@/auth/client'

export function UserInfo() {
  const { data: session, status } = useSession()

  if (status === 'loading') return Loading...
  if (!session?.user) return Not signed in

  return {session.user.name}
}
```

### Конфигурация Auth.js (NextAuth v5)

Каноническая конфигурация auth находится в `auth.ts` в корне проекта. Она настраивает:
- Провайдера учетных данных (email/пароль)
- Провайдера Google OAuth
- Стратегию JWT (с ротацией токенов обновления)
- Адаптер PostgreSQL (`lib/auth/postgres-adapter.ts`)

Не создавайте дополнительных файлов `auth.ts` в функциональных модулях. Расширяйте корневую конфигурацию.

> **Warning**
> Никогда не передавайте `userId` от клиента в серверное действие или API-маршрут. Получайте его из `await auth()` на сервере. См. `app/_actions/fcm.ts` для канонического паттерна — `userId получается только из auth() — никогда не передавайте userId от клиента`.

## Push-уведомления (FCM)

Регистрация и отмена регистрации FCM-токенов происходят через серверные действия в `app/_actions/fcm.ts`. Соответствующие операции с БД находятся в `lib/notifications/fcm-token-db.ts`.

```typescript
'use server'

import { auth } from '@/auth'
import { upsertFcmTokenForUser, unregisterFcmTokenForUser } from '@/lib/notifications/fcm-token-db'
import type { UpsertFcmTokenParams, UpsertFcmTokenResult } from '@/lib/notifications/fcm-token-db'

export async function upsertFcmToken(params: UpsertFcmTokenParams): Promise {
  const session = await auth()
  if (!session?.user?.id) return { error: 'Authentication required' }
  return upsertFcmTokenForUser(session.user.id, params)
}
```

Отправляйте уведомления на стороне сервера с помощью инструмента `ring-mcp_ring-fcm-send` или прямого обмена сообщениями Firebase Admin. Никогда не раскрывайте серверные ключи FCM клиенту.

## Платежи (WayForPay)

Ring Platform использует WayForPay в качестве основного платежного провайдера. Ключевые паттерны:

- **Вся логика платежей исключительно на сервере** — никогда не раскрывайте API-ключи или HMAC-секреты клиенту.
- **Идемпотентный `orderReference`** — каждая попытка платежа имеет уникальный `orderReference` для предотвращения дублирования списаний.
- **HMAC-верификация** — обратные вызовы вебхуков верифицируются с помощью HMAC MD5 перед обработкой.
- **Обработка 3DS** — перенаправления WayForPay управляются через `components/store/checkout/`.

См. `docs/en/architecture/payment-conductor.mdx` для полной архитектуры платежей, потока вебхуков и паттернов возврата средств.

## Границы серверных и клиентских компонентов

- **По умолчанию используйте серверные компоненты** — получайте данные и рендерьте на сервере.
- **Опускайте клиентские границы вниз** — оборачивайте интерактивные элементы (формы, карты, реальное время) в листьях `'use client'`.
- **Избегайте useEffect для получения данных** — используйте серверные действия, обработчики маршрутов или `useActionState` в React 19.

```typescript
// page.tsx — серверный компонент (по умолчанию)
export default async function EntitiesPage({ params: { locale } }: Props) {
  const session = await auth()

  const result = await db().queryDocs({
    collection: 'entities',
    filters: [],
    pagination: { limit: 20 },
  })

  return 
}
```

## Загрузка файлов

Используйте абстракцию `file()` из `@/lib/file` вместо прямого вызова API Vercel Blob или S3.

```typescript
import { file } from '@/lib/file'

const result = await file().upload(`entities/${entityId}/logo.webp', fileObject, {
  access: 'public',
  addRandomSuffix: false,
})
if (!result.success) throw new Error(result.error)
```

## Паттерны тестирования

Тесты находятся в `__tests__/` и используют Jest (с `@jest-environment jsdom` для тестов компонентов) или Vitest.

### Имитируйте внешние сервисы, а не DatabaseService

Тесты Ring Platform имитируют на границе — константы, `next/link`, пакеты переводов — но **не** имитируют `@/lib/database` с фейковым Firestore. Логика, зависящая от базы данных, тестируется либо на реальной тестовой базе данных, либо через интеграционные тесты, которые используют полный стек адаптеров.

> **Tip**
> См. [Руководство по тестированию](/en/docs/development/testing) для полной стратегии тестирования и просмотрите реальные тестовые файлы в `__tests__/features/` (например, `news-visibility-filter.test.ts`, `referral-commission.test.ts`) для паттернов, тестирующих чистую бизнес-логику без имитации базы данных.

### Пример тестирования компонентов

```typescript
/**
 * @jest-environment jsdom
 */

import { render, screen } from '@testing-library/react'
import { NextIntlClientProvider } from 'next-intl'
import EntityCard from '@/components/EntityCard'
import type { Locale } from '@/i18n/shared'

// Имитация next/link
jest.mock('next/link', () => {
  return ({ children, href }: { children: React.ReactNode; href: string }) => (
    {children}
  )
})

const mockMessages = {
  'entities.card.viewDetails': 'View Details',
}

describe('EntityCard', () => {
  it('рендерит название сущности', () => {
    const entity = { id: '1', name: 'Test Entity', type: 'technology' }

    render(
      
        
      
    )

    expect(screen.getByText('Test Entity')).toBeInTheDocument()
  })
})
```

## Производительность

### Паттерны запросов к базе данных

- Используйте `db().queryDocs()` с `filters` и `pagination`, чтобы ограничить результаты на уровне базы данных — не получайте все документы и не фильтруйте в коде приложения.
- Используйте `db().findDocById()` для поиска одного документа вместо `queryDocs({ filters: [{ field: 'id', operator: '==', value: id }] })`.
- Кэш сущностей в `DatabaseService` обеспечивает TTL 30 секунд для согласованности чтения после записи в коллекции `entities` — ручное кэширование не требуется для базовых случаев.

### Производительность React

- Серверные компоненты по умолчанию — нулевой клиентский JS для чистого рендеринга.
- `useActionState` (React 19) для управления состоянием формы в клиентских компонентах — не требуется внешняя библиотека состояния.
- Динамические импорты с `next/dynamic` для тяжелых клиентских компонентов (карты, редакторы, графики).

## Организация кода

- **`app/_actions/`** — все серверные действия, один файл на домен.
- **`features/{domain}/services/`** — бизнес-логика, которую можно вызывать из серверных действий или обработчиков маршрутов.
- **`features/{domain}/lib/`** — доменно-специфичные чистые функции и утилиты.
- **`lib/database/`** — абстракция базы данных. Не добавляйте сюда специфичные для функций запросы к базе данных.
- **`components/`** — только общие UI-компоненты. Специфичные для функций компоненты находятся в `features/{domain}/components/`.

См. [Структура кода](/en/docs/development/code-structure) для полной схемы директорий.

---

*Далее: [Структура кода](/en/docs/development/code-structure) — компоновка App Router, модули функций и организация файлов.*
*См. также: [Руководство по тестированию](/en/docs/development/testing), [Режимы бэкенда и базы данных](/en/docs/architecture/backend-modes-and-databases)*
