Модель запрос-ответ в Iron

Библиотека Iron строит свою работу вокруг классической модели request–response, в которой каждый входящий запрос обрабатывается изолированно и приводит к формированию единственного ответа. В отличие от событийных или потоковых систем, здесь сохраняется строгая линейность: запрос → обработка → ответ.

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


Объект запроса (Request)

Запрос в Iron представляет собой абстракцию над HTTP-входящими данными. Он инкапсулирует:

  • метод (GET, POST, PUT, DELETE)
  • заголовки
  • параметры URL
  • тело запроса
  • метаданные соединения

Пример структуры:

{
  method: 'POST',
  url: '/api/users',
  headers: { 'content-type': 'application/json' },
  body: { name: 'Alice' }
}

Iron не навязывает строгую форму хранения данных — объект запроса может быть расширен пользовательскими полями в процессе обработки.


Объект ответа (Response)

Ответ формируется отдельно и передаётся обратно клиенту после завершения обработки. Он включает:

  • HTTP-статус
  • заголовки
  • тело ответа

Минимальный пример:

{
  status: 200,
  headers: { 'content-type': 'application/json' },
  body: { success: true }
}

Iron поддерживает ленивое формирование ответа — данные могут добавляться по мере прохождения через обработчики.


Контекст обработки

Каждый запрос оборачивается в контекст — специальный объект, объединяющий request и response, а также дополнительные данные:

const context = {
  request,
  response,
  state: {}
}

Поле state используется для передачи данных между промежуточными слоями без загрязнения глобальной области.


Цепочка middleware

Iron реализует модель обработки через middleware — функции, выполняющиеся последовательно. Каждая функция получает контекст и управление:

async function middleware(ctx, next) {
  // логика до
  await next()
  // логика после
}

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

  • Линейность выполнения
  • Возможность прерывания цепочки
  • Асинхронность по умолчанию

Пример цепочки:

app.use(async (ctx, next) => {
  console.log('Request received')
  await next()
})

app.use(async (ctx) => {
  ctx.response.body = { message: 'Hello' }
})

Поток выполнения

Модель обработки запроса выглядит следующим образом:

  1. Приём HTTP-запроса
  2. Создание объекта request
  3. Инициализация response
  4. Формирование контекста
  5. Последовательный запуск middleware
  6. Финализация response
  7. Отправка ответа клиенту

Графически:

Request → Middleware 1 → Middleware 2 → ... → Response

Управление потоком

Middleware могут:

  • продолжить выполнение (await next())
  • остановить цепочку (не вызывая next)
  • изменить response на любом этапе

Пример прерывания:

app.use(async (ctx, next) => {
  if (!ctx.request.headers.authorization) {
    ctx.response.status = 401
    ctx.response.body = { error: 'Unauthorized' }
    return
  }
  await next()
})

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

Iron не скрывает ошибки, а делегирует их обработку middleware-уровню.

Базовый подход:

app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    ctx.response.status = 500
    ctx.response.body = { error: err.message }
  }
})

Это позволяет централизовать обработку исключений.


Иммутабельность и мутация

Хотя request считается неизменяемым источником данных, Iron допускает его расширение:

ctx.request.user = decodedToken

Response, наоборот, проектируется как изменяемый объект — он постепенно формируется в ходе обработки.


Асинхронная природа

Iron полностью основан на async/await, что позволяет:

  • работать с базами данных
  • вызывать внешние API
  • обрабатывать файлы

Пример:

app.use(async (ctx) => {
  const users = await db.getUsers()
  ctx.response.body = users
})

Параллельность и изоляция

Каждый запрос обрабатывается независимо:

  • нет разделяемого состояния по умолчанию
  • отсутствие гонок данных
  • масштабируемость через горизонтальное распределение

Расширяемость модели

Iron не ограничивает разработчика:

  • можно добавлять собственные свойства в контекст
  • реализовывать кастомные middleware
  • строить сложные пайплайны обработки

Пример расширения:

app.use(async (ctx, next) => {
  ctx.state.startTime = Date.now()
  await next()
  console.log(Date.now() - ctx.state.startTime)
})

Работа с телом запроса

Iron не навязывает способ парсинга тела — это ответственность middleware:

app.use(async (ctx, next) => {
  if (ctx.request.headers['content-type'] === 'application/json') {
    ctx.request.body = JSON.parse(ctx.request.rawBody)
  }
  await next()
})

Формирование ответа

Ответ можно формировать по частям:

app.use(async (ctx, next) => {
  ctx.response.headers['x-powered-by'] = 'Iron'
  await next()
})

app.use(async (ctx) => {
  ctx.response.body = { data: 'example' }
})

Статусы и заголовки

Iron не ограничивает работу с HTTP:

ctx.response.status = 201
ctx.response.headers['cache-control'] = 'no-cache'

Инкапсуляция логики

Каждый middleware выполняет одну задачу:

  • авторизация
  • логирование
  • валидация
  • бизнес-логика

Это соответствует принципу single responsibility.


Взаимодействие с маршрутизацией

Хотя сама модель request–response не включает маршрутизацию, она легко интегрируется:

app.use(async (ctx, next) => {
  if (ctx.request.url === '/users') {
    ctx.response.body = getUsers()
    return
  }
  await next()
})

Ленивое выполнение

Middleware вызываются только при наличии запроса. Нет фоновых процессов или скрытых вычислений.


Тестируемость

Изолированная модель позволяет тестировать обработчики без запуска сервера:

await middleware(ctx, async () => {})

Контроль над жизненным циклом

Каждый этап полностью контролируем:

  • создание контекста
  • запуск цепочки
  • формирование ответа

Нет скрытых механизмов.


Минимальный пример приложения

const app = new Iron()

app.use(async (ctx, next) => {
  console.log(ctx.request.method, ctx.request.url)
  await next()
})

app.use(async (ctx) => {
  ctx.response.status = 200
  ctx.response.body = { message: 'OK' }
})

app.listen(3000)

Ключевые свойства модели

  • детерминированность
  • предсказуемость
  • изоляция
  • расширяемость
  • асинхронность

Отличия от других подходов

В отличие от callback-ориентированных фреймворков:

  • отсутствует вложенность колбэков
  • используется линейный поток
  • проще отслеживать состояние

В отличие от потоковых систем:

  • нет сложного управления backpressure
  • упрощена логика обработки

Практические следствия

  • легко добавлять новые middleware
  • просто внедрять кросс-срезную логику (логирование, auth)
  • высокая читаемость кода
  • удобная отладка

Ограничения модели

  • не подходит для real-time потоков (WebSocket)
  • требует явного управления состоянием
  • последовательность выполнения может влиять на результат

Итеративная обработка

Возможна организация циклов обработки внутри middleware:

app.use(async (ctx, next) => {
  for (const item of ctx.request.body.items) {
    // обработка
  }
  await next()
})

Комбинирование middleware

Middleware можно группировать:

const auth = async (ctx, next) => { /* ... */ }
const validate = async (ctx, next) => { /* ... */ }

app.use(auth)
app.use(validate)

Финализация ответа

Если response не был установлен, Iron может:

  • вернуть пустой ответ
  • выбросить ошибку (в зависимости от реализации)

Поэтому важно явно задавать ctx.response.body.


Контроль времени выполнения

Добавление таймингов:

app.use(async (ctx, next) => {
  const start = Date.now()
  await next()
  ctx.response.headers['x-time'] = Date.now() - start
})

Масштабирование

Модель request–response хорошо масштабируется:

  • через кластеризацию Node.js
  • через балансировщики нагрузки
  • через контейнеризацию

Прозрачность данных

Каждый этап явно показывает:

  • что пришло
  • что изменилось
  • что отправляется

Это снижает сложность поддержки.


Итоговая структура взаимодействия

Client → Request → Context → Middleware Chain → Response → Client

Модель остаётся простой, но при этом достаточно мощной для построения сложных серверных приложений.