Проксирование API-запросов

При разработке веб-приложений фронтенд и серверная часть нередко работают на разных адресах. Например:

  • клиентское приложение доступно по адресу http://localhost:1234;
  • API-сервер работает на http://localhost:3000.

Попытка выполнить запрос из браузера к другому источнику может привести к ограничениям политики безопасности браузера (Same-Origin Policy) и возникновению ошибок CORS.

Проксирование API-запросов позволяет перенаправлять обращения клиента через сервер разработки Parcel. В результате браузер взаимодействует только с одним источником, а Parcel незаметно передаёт запросы нужному API-серверу.

Схема работы выглядит следующим образом:

Браузер
    │
    ▼
Parcel Dev Server
    │
    ▼
API-сервер

Такой подход упрощает разработку и избавляет от необходимости изменять настройки CORS на сервере.


Проблема CORS при локальной разработке

Предположим, приложение выполняет следующий запрос:

fetch('http://localhost:3000/api/users')
  .then(response => response.json())
  .then(data => console.log(data));

Если страница открыта по адресу:

http://localhost:1234

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

Даже при одинаковом домене:

localhost:1234
localhost:3000

это уже два разных источника.

При отсутствии соответствующих CORS-заголовков сервер может вернуть ошибку:

Access to fetch at 'http://localhost:3000/api/users'
from origin 'http://localhost:1234'
has been blocked by CORS policy

Прокси решает эту проблему путём промежуточной обработки запросов.


Идея использования прокси

Вместо прямого обращения к API:

fetch('http://localhost:3000/api/users')

используется относительный путь:

fetch('/api/users')

Для браузера запрос отправляется на тот же источник:

http://localhost:1234/api/users

Далее Parcel принимает запрос и перенаправляет его на настоящий сервер:

http://localhost:3000/api/users

Для клиентского кода различия отсутствуют.


Настройка проксирования через package.json

Parcel позволяет задавать прокси через поле proxy.

Пример:

{
  "name": "parcel-app",
  "proxy": "http://localhost:3000"
}

После запуска сервера разработки:

parcel serve src/index.html

все неизвестные маршруты будут автоматически перенаправляться на указанный сервер.

Теперь запрос:

fetch('/api/users')

будет преобразован в:

http://localhost:3000/api/users

Пример структуры проекта

project/
│
├── src/
│   ├── index.html
│   ├── app.js
│   └── styles.css
│
├── package.json
└── .parcelrc

Файл package.json:

{
  "name": "parcel-demo",
  "version": "1.0.0",
  "proxy": "http://localhost:3000"
}

Файл app.js:

async function loadUsers() {
  const response = await fetch('/api/users');
  const users = await response.json();

  console.log(users);
}

loadUsers();

В результате приложение не зависит от конкретного адреса сервера.


Работа с REST API

Прокси одинаково работает для любых HTTP-методов.

GET

fetch('/api/products');

После перенаправления:

GET http://localhost:3000/api/products

POST

fetch('/api/products', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: 'Laptop'
  })
});

Прокси передаст:

POST http://localhost:3000/api/products

вместе со всеми заголовками и телом запроса.

PUT

fetch('/api/products/10', {
  method: 'PUT',
  body: JSON.stringify({
    title: 'Updated laptop'
  })
});

DELETE

fetch('/api/products/10', {
  method: 'DELETE'
});

Все методы работают одинаковым образом.


Использование с Axios

Прокси не зависит от используемой HTTP-библиотеки.

Пример с Axios:

import axios from 'axios';

async function getPosts() {
  const response = await axios.get('/api/posts');

  console.log(response.data);
}

getPosts();

Parcel перенаправит запрос на API-сервер точно так же, как и в случае с Fetch API.


Проксирование запросов к GraphQL

GraphQL-серверы также часто располагаются отдельно от клиентского приложения.

Запрос:

fetch('/graphql', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    query: `
      query {
        users {
          id
          name
        }
      }
    `
  })
});

После обработки прокси:

POST http://localhost:3000/graphql

Для клиента взаимодействие остаётся прозрачным.


Работа с аутентификацией

Прокси особенно полезен при использовании авторизации на основе cookies.

Пример:

fetch('/api/profile', {
  credentials: 'include'
});

Браузер считает запрос локальным:

http://localhost:1234/api/profile

Поэтому работа с cookie обычно становится значительно проще по сравнению с прямыми междоменными запросами.


Использование переменных окружения

Часто адрес API различается для разных окружений.

Файл .env:

API_URL=http://localhost:3000

Клиентский код:

console.log(process.env.API_URL);

Однако при использовании прокси фронтенд может вообще не знать реальный адрес сервера:

fetch('/api/users');

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


Отличия прокси от прямого обращения к API

Характеристика Прямой запрос Через прокси
Возможны CORS-ошибки Да Нет
Требуется настройка сервера Часто Обычно нет
Используются абсолютные URL Да Нет
Удобство локальной разработки Среднее Высокое
Единая точка доступа Нет Да

Отладка проксируемых запросов

Для проверки работы прокси удобно использовать вкладку Network в инструментах разработчика браузера.

Клиентский запрос:

fetch('/api/users');

В панели разработчика будет отображаться:

http://localhost:1234/api/users

Хотя фактически Parcel перенаправляет запрос на внешний сервер.

При возникновении ошибок необходимо проверить:

  1. Запущен ли API-сервер.
  2. Правильно ли указан адрес прокси.
  3. Существует ли требуемый маршрут.
  4. Возвращает ли сервер корректный ответ.
  5. Не блокируется ли соединение брандмауэром или сетевыми правилами.

Обработка ошибок при работе через прокси

Даже при корректной настройке прокси сервер может быть недоступен.

Пример безопасной обработки:

async function loadData() {
  try {
    const response = await fetch('/api/users');

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    const data = await response.json();

    return data;
  } catch (error) {
    console.error('Ошибка запроса:', error);
  }
}

Такой подход позволяет корректно реагировать на:

  • ошибки сети;
  • недоступность API;
  • ответы 404;
  • ответы 500;
  • ошибки обработки данных.

Типичный сценарий разработки

Предположим, имеются два независимых проекта.

Сервер:

http://localhost:5000

Клиент на Parcel:

http://localhost:1234

Настройка:

{
  "proxy": "http://localhost:5000"
}

Клиентский код:

const response = await fetch('/api/orders');

Последовательность действий:

1. Браузер отправляет запрос на localhost:1234.
2. Parcel принимает запрос.
3. Parcel пересылает его на localhost:5000.
4. Сервер формирует ответ.
5. Parcel возвращает результат браузеру.

Клиентское приложение при этом остаётся полностью независимым от фактического расположения серверной части.


Практические рекомендации

Использование относительных URL

Предпочтительно:

fetch('/api/users');

Вместо:

fetch('http://localhost:3000/api/users');

Единый префикс API

Удобно группировать все серверные маршруты:

/api/users
/api/posts
/api/orders

Это облегчает настройку прокси и сопровождение проекта.

Изоляция конфигурации

Адреса серверов должны храниться в конфигурационных файлах, а не в исходном коде.

Проверка сетевых запросов

Вкладка Network позволяет быстро определить, возникла проблема в клиентском коде, в прокси или на стороне сервера.

Разделение среды разработки и продакшена

Прокси используется главным образом во время локальной разработки. В производственной среде обычно применяется обратный прокси на базе веб-сервера, например Nginx или Apache HTTP Server, который выполняет аналогичную задачу уже на уровне инфраструктуры.