В основе обработки запросов в Iron лежит концепция серверных экшенов — функций, которые напрямую описывают бизнес-логику, выполняемую на стороне сервера в ответ на HTTP-запрос. Такой подход позволяет рассматривать каждый маршрут не как набор разрозненных обработчиков, а как композицию действий, работающих с единым контекстом запроса.
Серверный экшен в Iron — это асинхронная функция, получающая контекст запроса и формирующая ответ. В отличие от классических handler-функций, экшены чаще проектируются как переиспользуемые единицы логики, которые могут быть подключены к маршрутам, middleware-цепочкам и сервисам.
Каждый серверный экшен получает объект контекста, содержащий данные запроса и инструменты ответа. Типичный контекст включает:
Структура контекста позволяет минимизировать зависимость от глобальных объектов и делает экшены изолированными.
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 в 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 серверные экшены часто комбинируются с отдельным слоем валидации. Это позволяет отделить проверку данных от бизнес-логики.
Валидация может выполняться:
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-слою. Они могут использоваться:
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 };
}
Сервисы выступают промежуточным слоем между экшенами и инфраструктурой.
Безопасность реализуется на нескольких уровнях:
const requireAdmin = async (ctx, next) => {
if (ctx.user.role !== 'admin') {
ctx.status = 403;
return { error: 'Forbidden' };
}
await next();
};
Серверные экшены не должны самостоятельно реализовывать сложную логику безопасности — она выносится в отдельные слои.
Проектирование серверных экшенов в Iron предполагает предсказуемость поведения при повторных вызовах. Это особенно важно для POST/PUT операций.
Подходы:
Экшены становятся детерминированными единицами логики, что упрощает масштабирование и отладку.
Архитектура 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-серверу позволяет тестировать экшены как обычные функции.