Все статьиArchitecture

Чистая архитектура в Next.js App Router: гайд по FSD

Внедрение чистой архитектуры и Feature-Sliced Design в Next.js App Router. Разделение слоев, Server Actions и репозитории — закажите архитектуру в EvaLab!

10 мин readОбновлено 28.08.2026
Чистая архитектура в Next.js App Router: гайд по FSD — статья блога EvaLab

С появлением App Router и серверных компонентов (React Server Components) фреймворк Next.js стирает грань между традиционным фронтендом и бэкендом. Разработчики получили возможность обращаться к базе данных, сторонним API и микросервисам напрямую из компонента или Server Action.

Однако свобода имеет обратную сторону: в крупных корпоративных веб-приложениях бизнес-логика, UI-верстка и SQL-запросы быстро смешиваются в один «спагетти-код». В результате рефакторинг становится болезненным, а написание unit-тестов — практически невозможным.

В этой статье команда EvaLab детально опишет внедрение чистой архитектуры (Clean Architecture) и концепции Feature-Sliced Design (FSD) в экосистему Next.js App Router.

Почему стандартная папочная структура ломается на масштабе

По умолчанию Next.js предлагает файловый роутинг в папке app/. В небольших проектах на 5–10 страниц создатели размещают Server Actions прямо рядом с компонентами или создают хаотичные папки components/, lib/, utils/.

С ростом проекта возникают циклические импорты, сильная связность (tight coupling) между UI и базами данных, а также дублирование бизнес-правил.

Цели Чистой Архитектуры в Next.js:

  1. Независимость от фреймворка: core-логика бизнеса не должна зависеть от того, используете ли вы Next.js, Express или NestJS.
  2. Тестируемость: возможность протестировать доменную модель и юзкейсы без запуска серверного окружения Next.js или браузера.
  3. Заменяемость инфраструктуры: лёгкая смена ORM (например, Prisma на Drizzle) или провайдера базы данных без изменения UI-компонентов.

Слои архитектуры и адаптация Feature-Sliced Design (FSD)

Мы рекомендуем гибридный подход: адаптированный Feature-Sliced Design (FSD) с четким слоистым делением по правилам Дяди Боба (Uncle Bob Clean Architecture).

src/
├── app/                  # Next.js App Router (только роутинг, layouts, metadata)
├── processes/            # Сложные сквозные сценарии (авторизация, оформление заказа)
├── pages/                # Страничные композиции (Page Slices)
├── widgets/              # Крупные самостоятельные блоки UI (Header, Sidebar, CatalogGrid)
├── features/             # Пользовательские фичи (AddToCart, FilterProducts, ToggleTheme)
├── entities/             # Бизнес-сущности (User, Order, Product, Cart)
└── shared/               # Инфраструктура, UI-kit, API-клиенты, базовые утилиты

Главные правила импортов:

  • Модули из высших слоев могут импортировать только модули из нижних слоев (app -> pages -> widgets -> features -> entities -> shared).
  • Импорт «снизу вверх» или горизонтальный импорт между модулями одного слоя запрещен.

Разделение Domain, Application и Infrastructure слоев

Внутри слоя entities или отдельного модуля бизнес-логики мы выделяем 3 подслоя:

  1. Domain Layer: интерфейсы, доменные типы, валидаторы чистых сущностей.
  2. Application Layer (Use Cases): сценарии использования бизнеса (например, createOrderUseCase).
  3. Infrastructure Layer: реализация работы с базами данных (Prisma, PostgreSQL), внешними API и почтовыми сервисами.

Реализация Pattern Repository с TypeScript и Server Actions

Рассмотрим пример управления заказами.

1. Доменный слой (entities/order/domain/types.ts)

export interface Order {
  id: string;
  userId: string;
  totalAmount: number;
  status: 'pending' | 'paid' | 'shipped';
  createdAt: Date;
}

export interface IOrderRepository {
  findById(id: string): Promise<Order | null>;
  create(data: Omit<Order, 'id' | 'createdAt' | 'status'>): Promise<Order>;
}

2. Инфраструктурный слой (entities/order/infrastructure/prisma-order.repository.ts)

import { IOrderRepository, Order } from '../domain/types';
import { db } from '@/shared/lib/db'; // Prisma client instance

export class PrismaOrderRepository implements IOrderRepository {
  async findById(id: string): Promise<Order | null> {
    const record = await db.order.findUnique({ where: { id } });
    if (!record) return null;

    return {
      id: record.id,
      userId: record.userId,
      totalAmount: record.totalAmount,
      status: record.status as Order['status'],
      createdAt: record.createdAt,
    };
  }

  async create(data: Omit<Order, 'id' | 'createdAt' | 'status'>): Promise<Order> {
    const created = await db.order.create({
      data: {
        userId: data.userId,
        totalAmount: data.totalAmount,
        status: 'pending',
      },
    });

    return {
      id: created.id,
      userId: created.userId,
      totalAmount: created.totalAmount,
      status: created.status as Order['status'],
      createdAt: created.createdAt,
    };
  }
}

3. Слой приложения и Server Action (features/create-order/actions/create-order.action.ts)

'use server';

import { PrismaOrderRepository } from '@/entities/order/infrastructure/prisma-order.repository';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';

const createOrderSchema = z.object({
  userId: z.string().uuid(),
  totalAmount: z.number().positive(),
});

export async function createOrderAction(formData: FormData) {
  const repository = new PrismaOrderRepository();

  const validated = createOrderSchema.parse({
    userId: formData.get('userId'),
    totalAmount: Number(formData.get('totalAmount')),
  });

  // Бизнес-логика создания
  const newOrder = await repository.create(validated);

  revalidatePath('/dashboard/orders');
  return { success: true, orderId: newOrder.id };
}

Тестируемость архитектуры

Благодаря тому, что бизнес-логика опирается на интерфейс IOrderRepository, мы можем написать мгновенные Unit-тесты с использованием MockOrderRepository без поднятия базы данных или моков Next.js:

import { describe, it, expect } from 'vitest';
import { MockOrderRepository } from './mock-order.repository';

describe('Order UseCases', () => {
  it('should calculate discount correctly', async () => {
    const mockRepo = new MockOrderRepository();
    // Тестирование чистой доменной логики
    const result = await mockRepo.create({ userId: 'user-1', totalAmount: 1000 });
    expect(result.status).toBe('pending');
  });
});

Заключение

Чистая архитектура и Feature-Sliced Design в Next.js App Router дают предсказуемость, легкую масштабируемость и высокое качество кода в продуктах любой сложности. Такой подход избавляет команду от хаоса при росте проекта и снижает технический долг до минимума.

Планируете разработку сложного сервиса на Next.js или хотите отрефакторить имеющийся монолит? Закажите проектирование архитектуры в веб-агентстве EvaLab — мы строим отказоустойчивые веб-системы по мировым стандартам!

Следующий шаг

Превратим идею в рабочую систему

Разберём задачу, сопоставим её с целями бизнеса и предложим план внедрения с понятными этапами и критериями результата.