Серверные экшены и Iron

В основе обработки запросов в Iron лежит концепция серверных экшенов — функций, которые напрямую описывают бизнес-логику, выполняемую на стороне сервера в ответ на HTTP-запрос. Такой подход позволяет рассматривать каждый маршрут не как набор разрозненных обработчиков, а как композицию действий, работающих с единым контекстом запроса.

Серверный экшен в Iron — это асинхронная функция, получающая контекст запроса и формирующая ответ. В отличие от классических handler-функций, экшены чаще проектируются как переиспользуемые единицы логики, которые могут быть подключены к маршрутам, middleware-цепочкам и сервисам.

Контекст выполнения серверного экшена

Каждый серверный экшен получает объект контекста, содержащий данные запроса и инструменты ответа. Типичный контекст включает:

  • параметры маршрута
  • query-строку
  • тело запроса
  • заголовки
  • объект ответа
  • состояние сессии (если подключено)
  • ссылки на сервисы приложения

Структура контекста позволяет минимизировать зависимость от глобальных объектов и делает экшены изолированными.

async function getUserAction(ctx) {
  const { id } = ctx.params;
  const user = await ctx.services.user.findById(id);

  if (!user) {
    ctx.status = 404;
    return { error: 'User not found' };
  }

  return { data: user };
}

Контекст в Iron выступает единой точкой доступа ко всем слоям приложения, что упрощает тестирование и масштабирование.

Привязка серверных экшенов к маршрутам

Маршрутизация в Iron строится вокруг явного связывания HTTP-методов и URL с серверными экшенами. Это позволяет отделить описание маршрутов от их реализации.

router.get('/users/:id', getUserAction);
router.post('/users', createUserAction);
router.put('/users/:id', updateUserAction);
router.delete('/users/:id', deleteUserAction);

Каждый экшен остаётся независимым и может использоваться в других частях системы, например, в фоновых задачах или композициях.

Композиция серверных экшенов

Одной из ключевых особенностей Iron является возможность композиции экшенов. Экшен может быть собран из нескольких функций, каждая из которых выполняет свою часть работы.

Типичный сценарий композиции:

  • валидация входных данных
  • выполнение бизнес-логики
  • постобработка результата
const validateUser = async (ctx, next) => {
  if (!ctx.body.email) {
    ctx.status = 400;
    return { error: 'Email is required' };
  }
  await next();
};

const createUserAction = async (ctx) => {
  const user = await ctx.services.user.create(ctx.body);
  return { data: user };
};

router.post('/users', validateUser, createUserAction);

Такой подход снижает дублирование и формирует прозрачную цепочку обработки запроса.

Асинхронная модель выполнения

Серверные экшены в Iron изначально рассчитаны на асинхронное выполнение. Это критически важно для работы с базами данных, внешними API и файловой системой.

Основные особенности:

  • поддержка async/await
  • автоматическое ожидание завершения цепочки middleware
  • централизованная обработка ошибок

Асинхронность не требует дополнительной конфигурации и встроена в жизненный цикл запроса.

Middleware как часть экшен-цепочки

Middleware в Iron тесно связан с серверными экшенами. Он может:

  • модифицировать контекст
  • выполнять предварительную проверку
  • завершать запрос
  • передавать управление дальше
const authMiddleware = async (ctx, next) => {
  const token = ctx.headers.authorization;

  if (!token) {
    ctx.status = 401;
    return { error: 'Unauthorized' };
  }

  ctx.user = await ctx.services.auth.verify(token);
  await next();
};

Middleware формирует слой между HTTP-интерфейсом и бизнес-логикой, не нарушая изоляцию экшенов.

Обработка ошибок в серверных экшенах

Iron использует централизованную модель обработки ошибок, при которой исключения перехватываются на уровне выполнения экшена или middleware-цепочки.

const errorHandler = async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.status = err.status || 500;
    ctx.body = {
      error: err.message
    };
  }
};

Серверные экшены могут выбрасывать ошибки напрямую, не заботясь о формировании HTTP-ответа.

async function getProduct(ctx) {
  const product = await ctx.services.product.find(ctx.params.id);

  if (!product) {
    throw new Error('Product not found');
  }

  return { data: product };
}

Валидация входных данных

В Iron серверные экшены часто комбинируются с отдельным слоем валидации. Это позволяет отделить проверку данных от бизнес-логики.

Валидация может выполняться:

  • middleware-слоем
  • отдельными валидирующими экшенами
  • схемами
const validateProduct = async (ctx, next) => {
  const { name, price } = ctx.body;

  if (!name || typeof price !== 'number') {
    ctx.status = 400;
    return { error: 'Invalid payload' };
  }

  await next();
};

Такой подход уменьшает связанность и повышает читаемость бизнес-экшенов.

Переиспользование серверных экшенов

Серверные экшены в Iron не привязаны исключительно к HTTP-слою. Они могут использоваться:

  • в фоновых задачах
  • в очередях
  • в cron-джобах
  • внутри других экшенов
async function syncUsersJob(ctx) {
  const users = await ctx.services.external.fetchUsers();

  for (const user of users) {
    await ctx.services.user.upsert(user);
  }

  return { processed: users.length };
}

Это превращает экшены в универсальные единицы бизнес-логики.

Работа с состоянием и сервисами

Контекст Iron обычно содержит ссылку на слой сервисов. Это позволяет экшенам не зависеть от конкретных реализаций хранилищ данных.

Пример взаимодействия:

async function updateProfile(ctx) {
  const updated = await ctx.services.profile.update(
    ctx.params.id,
    ctx.body
  );

  return { data: updated };
}

Сервисы выступают промежуточным слоем между экшенами и инфраструктурой.

Безопасность серверных экшенов

Безопасность реализуется на нескольких уровнях:

  • middleware аутентификации
  • контроль прав доступа
  • фильтрация входных данных
  • ограничение частоты запросов
const requireAdmin = async (ctx, next) => {
  if (ctx.user.role !== 'admin') {
    ctx.status = 403;
    return { error: 'Forbidden' };
  }

  await next();
};

Серверные экшены не должны самостоятельно реализовывать сложную логику безопасности — она выносится в отдельные слои.

Идемпотентность и предсказуемость экшенов

Проектирование серверных экшенов в Iron предполагает предсказуемость поведения при повторных вызовах. Это особенно важно для POST/PUT операций.

Подходы:

  • явное разделение create/update/delete
  • использование уникальных идентификаторов операций
  • контроль побочных эффектов внутри сервисов

Экшены становятся детерминированными единицами логики, что упрощает масштабирование и отладку.

Паттерн «тонкие экшены, толстые сервисы»

Архитектура Iron обычно смещает бизнес-логику из экшенов в сервисный слой. Экшены остаются тонкими координаторами.

async function checkout(ctx) {
  const order = await ctx.services.order.create(ctx.user.id, ctx.body);
  await ctx.services.payment.process(order.id);

  return { data: order };
}

Такой подход снижает сложность маршрутов и упрощает тестирование.

Тестирование серверных экшенов

Серверные экшены легко тестируются благодаря чистому контексту.

test('getUserAction returns user data', async () => {
  const ctx = createMockContext({
    params: { id: 1 },
    services: mockServices
  });

  const result = await getUserAction(ctx);

  expect(result.data.id).toBe(1);
});

Отсутствие жёсткой привязки к HTTP-серверу позволяет тестировать экшены как обычные функции.