devServer.proxy: проксирование API-запросов

Во время разработки фронтенд-приложение часто запускается отдельно от backend-сервера. Например:

  • frontend работает на http://localhost:8080;
  • backend API работает на http://localhost:3000.

При попытке выполнить запрос:

fetch('/api/users')

браузер обращается к localhost:8080/api/users, а backend находится на другом порту. Если указать полный адрес:

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

возникают проблемы:

  • CORS;
  • различие URL между development и production;
  • необходимость хранить адрес API в конфигурации;
  • сложности с cookie и авторизацией.

Механизм devServer.proxy позволяет webpack-dev-server выступать промежуточным сервером и перенаправлять запросы на backend.

Схема работы:

Браузер → webpack-dev-server → backend API

Frontend продолжает обращаться к относительным URL:

fetch('/api/users')

Но webpack-dev-server автоматически проксирует запрос на:

http://localhost:3000/api/users

Базовая настройка proxy

Простейшая конфигурация:

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000'
      }
    }
  }
};

Теперь все запросы, начинающиеся с /api, будут перенаправляться на backend.

Пример:

/api/users

становится:

http://localhost:3000/api/users

Установка webpack-dev-server

Для работы proxy необходим webpack-dev-server.

Установка:

npm install webpack-dev-server --save-dev

Либо:

yarn add webpack-dev-server --dev

Запуск:

npx webpack serve

или через package.json:

{
  "scripts": {
    "dev": "webpack serve"
  }
}

Полный пример конфигурации

const path = require('path');

module.exports = {
  mode: 'development',

  entry: './src/index.js',

  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },

  devServer: {
    static: {
      directory: path.join(__dirname, 'public')
    },

    port: 8080,

    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        secure: false,
        changeOrigin: true
      }
    }
  }
};

Как работает маршрутизация proxy

Webpack анализирует URL запроса.

Если путь совпадает с ключом proxy:

proxy: {
  '/api': {
    target: 'http://localhost:3000'
  }
}

то запрос перенаправляется.

Примеры

Запрос Результат
/api/users proxy
/api/posts proxy
/images/logo.png обычная раздача
/main.js обычная раздача

Опция target

target определяет backend-сервер.

proxy: {
  '/api': {
    target: 'http://localhost:3000'
  }
}

Можно использовать:

target: 'https://api.example.com'

или:

target: 'http://127.0.0.1:5000'

Опция changeOrigin

Некоторые backend-серверы проверяют заголовок Host.

Если frontend работает на:

localhost:8080

а backend ожидает:

localhost:3000

то запрос может быть отклонён.

Решение:

changeOrigin: true

Пример:

proxy: {
  '/api': {
    target: 'http://localhost:3000',
    changeOrigin: true
  }
}

Webpack изменит origin запроса автоматически.


Опция secure

Если backend использует самоподписанный SSL-сертификат:

https://localhost:5000

может возникнуть ошибка TLS.

Для отключения проверки сертификата:

secure: false

Пример:

proxy: {
  '/api': {
    target: 'https://localhost:5000',
    secure: false
  }
}

Проксирование нескольких API

В одном проекте может быть несколько backend-сервисов.

Пример:

devServer: {
  proxy: {
    '/api': {
      target: 'http://localhost:3000'
    },

    '/auth': {
      target: 'http://localhost:4000'
    },

    '/upload': {
      target: 'http://localhost:5000'
    }
  }
}

Маршрутизация:

URL Сервер
/api/users 3000
/auth/login 4000
/upload/file 5000

Перезапись путей через pathRewrite

Иногда backend не использует тот же префикс.

Frontend:

/api/users

Backend:

/users

Решение:

proxy: {
  '/api': {
    target: 'http://localhost:3000',

    pathRewrite: {
      '^/api': ''
    }
  }
}

Теперь:

/api/users

превращается в:

/users

Сложные правила pathRewrite

Можно выполнять более сложные преобразования.

Пример:

pathRewrite: {
  '^/api/v1': '/v2'
}

Результат:

/api/v1/users

становится:

/v2/users

Использование функции в pathRewrite

pathRewrite поддерживает функцию.

pathRewrite: (path, req) => {
  return path.replace('/api', '');
}

Пример с логикой:

pathRewrite: (path, req) => {
  console.log(req.method);

  return path.replace('/api', '');
}

Опция logLevel

Позволяет управлять логированием proxy.

proxy: {
  '/api': {
    target: 'http://localhost:3000',
    logLevel: 'debug'
  }
}

Возможные значения:

Значение Описание
silent без логов
error только ошибки
warn предупреждения
info информационные сообщения
debug максимальная детализация

Отладка proxy

Часто используется:

logLevel: 'debug'

Пример:

proxy: {
  '/api': {
    target: 'http://localhost:3000',
    logLevel: 'debug'
  }
}

В консоли отображаются:

  • URL запроса;
  • rewrite path;
  • backend target;
  • ошибки подключения;
  • redirect;
  • заголовки.

Проксирование WebSocket

Webpack умеет проксировать WebSocket-соединения.

Пример:

proxy: {
  '/socket': {
    target: 'ws://localhost:5000',
    ws: true
  }
}

Теперь WebSocket:

ws://localhost:8080/socket

будет перенаправлен на:

ws://localhost:5000/socket

Backend часто устанавливает cookie:

Set-Cookie: token=123

Proxy способен корректно передавать cookie между браузером и backend.

Конфигурация:

proxy: {
  '/api': {
    target: 'http://localhost:3000',
    changeOrigin: true
  }
}

Проблемы SameSite и Secure

При авторизации возможны ограничения браузера:

SameSite=Lax
Secure

Особенно при HTTPS и разных origin.

Proxy уменьшает количество подобных проблем, потому что frontend работает через единый origin.


Proxy и CORS

Главное преимущество proxy — устранение CORS-проблем.

Без proxy:

localhost:8080 → localhost:3000

Браузер блокирует запрос.

С proxy:

localhost:8080 → localhost:8080/api

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


Настройка backend больше не нужна

Без proxy backend часто содержит:

app.use(cors());

или:

res.setHeader('Access-Control-Allow-Origin', '*');

При использовании proxy необходимость в этом во время разработки обычно исчезает.


Опция headers

Можно добавлять собственные заголовки.

proxy: {
  '/api': {
    target: 'http://localhost:3000',

    headers: {
      'X-Dev-Server': 'webpack'
    }
  }
}

Опция xfwd

Добавляет proxy-заголовки:

X-Forwarded-For
X-Forwarded-Host
X-Forwarded-Proto

Конфигурация:

proxy: {
  '/api': {
    target: 'http://localhost:3000',
    xfwd: true
  }
}

Опция timeout

Таймаут backend-запросов:

proxy: {
  '/api': {
    target: 'http://localhost:3000',
    timeout: 5000
  }
}

Опция proxyTimeout

Отдельный timeout для proxy-соединения:

proxy: {
  '/api': {
    target: 'http://localhost:3000',
    proxyTimeout: 5000
  }
}

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

При недоступности backend:

ECONNREFUSED

webpack-dev-server выводит ошибку в терминал.

Частые причины:

  • backend не запущен;
  • неверный порт;
  • ошибка SSL;
  • firewall;
  • неправильный target.

Использование функции bypass

Позволяет пропускать proxy для определённых запросов.

Пример:

proxy: {
  '/api': {
    target: 'http://localhost:3000',

    bypass: (req) => {
      if (req.headers.accept.includes('html')) {
        return '/index.html';
      }
    }
  }
}

Динамический выбор backend

Можно выбирать target программно.

Пример:

proxy: {
  '/api': {
    target: 'http://localhost:3000',

    router: (req) => {
      if (req.headers.host === 'admin.localhost') {
        return 'http://localhost:4000';
      }

      return 'http://localhost:3000';
    }
  }
}

Proxy для микросервисов

В крупных системах frontend взаимодействует с множеством сервисов:

/auth
/users
/payments
/notifications

Webpack proxy позволяет организовать единый gateway во время разработки.

Пример:

proxy: {
  '/auth': {
    target: 'http://localhost:3001'
  },

  '/users': {
    target: 'http://localhost:3002'
  },

  '/payments': {
    target: 'http://localhost:3003'
  }
}

Proxy и SPA

Single Page Application часто использует API:

/api/*

и frontend routing:

/profile
/settings
/dashboard

Важно не путать proxy и history fallback.

Правильная настройка:

devServer: {
  historyApiFallback: true,

  proxy: {
    '/api': {
      target: 'http://localhost:3000'
    }
  }
}

Proxy и React

Типичная структура:

React → localhost:8080
Express API → localhost:3000

Конфигурация:

devServer: {
  proxy: {
    '/api': {
      target: 'http://localhost:3000'
    }
  }
}

Запрос:

axios.get('/api/users');

Proxy и Vue

Пример:

devServer: {
  proxy: {
    '/api': {
      target: 'http://localhost:5000'
    }
  }
}

Proxy и Angular

Angular CLI использует похожую схему proxy через отдельный файл:

{
  "/api": {
    "target": "http://localhost:3000",
    "secure": false
  }
}

Но принцип полностью совпадает с webpack-dev-server.


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

Можно задавать массив объектов.

Пример:

proxy: [
  {
    context: ['/api'],
    target: 'http://localhost:3000'
  },

  {
    context: ['/auth'],
    target: 'http://localhost:4000'
  }
]

Proxy через регулярные выражения

Можно использовать шаблоны.

Пример:

proxy: {
  '^/api': {
    target: 'http://localhost:3000'
  }
}

Проверка работы proxy

Простейший тест:

fetch('/api/test')
  .then(r => r.json())
  .then(console.log);

Если backend отвечает корректно — proxy работает.


Типичные ошибки

Неверный target

Ошибка:

ECONNREFUSED

Причина:

target: 'http://localhost:9999'

при отсутствии сервера на порту 9999.


HTTPS mismatch

Ошибка:

socket hang up

Причина:

target: 'https://localhost:3000'

при HTTP backend.


Неверный pathRewrite

Frontend:

/api/users

Backend получает:

/api/users

хотя ожидает:

/users

Отсутствие changeOrigin

Некоторые backend-framework отклоняют запросы с неправильным Host.


Практическая конфигурация для modern frontend

devServer: {
  port: 8080,

  hot: true,

  historyApiFallback: true,

  proxy: {
    '/api': {
      target: 'http://localhost:3000',

      changeOrigin: true,

      secure: false,

      logLevel: 'debug',

      pathRewrite: {
        '^/api': ''
      }
    }
  }
}

Совместимость с Express backend

Backend:

const express = require('express');

const app = express();

app.get('/users', (req, res) => {
  res.json([
    { id: 1, name: 'Alex' }
  ]);
});

app.listen(3000);

Frontend:

fetch('/api/users')
  .then(r => r.json())
  .then(console.log);

Webpack:

proxy: {
  '/api': {
    target: 'http://localhost:3000',

    pathRewrite: {
      '^/api': ''
    }
  }
}

Архитектурная роль proxy

devServer.proxy решает сразу несколько задач:

  • изоляция frontend и backend;
  • устранение CORS;
  • единый origin;
  • маршрутизация API;
  • эмуляция production gateway;
  • поддержка cookie и авторизации;
  • работа с микросервисами;
  • упрощение frontend-кода.

Во многих современных проектах proxy становится обязательной частью локальной среды разработки, особенно при разделении frontend и backend на независимые приложения.