Твой сервер ходит в чужой API, а тот падает, тормозит и берёт деньги за каждый вызов. Учимся оборачивать, ретраить, кешировать и деградировать красиво.
На главной StudyCode продакт хочет виджет: «Москва −4°, снег». Свою метеостанцию мы ставить не будем 😄 — погоду отдают специальные сервисы: OpenWeather, WeatherAPI, open-meteo. Это third-party API — «чужой» API: ты его не писал, не хостишь и не можешь починить, когда он сломается.
Команда купила подписку на вымышленный MeteoAPI: 1000 бесплатных вызовов в день, дальше — по копейке за вызов.
Казалось бы: fetch('https://api.meteo.example/moscow?key=SECRET') прямо из браузера — и готово. Три причины, почему так никто не делает:
| Проблема | Что случится |
|---|---|
| 🔑 Ключ утечёт | API-ключ уедет в браузер каждому посетителю: F12 → вкладка Network — и вот он. Любой желающий сожжёт твой лимит за минуту |
| 🚧 CORS | Чужой API почти наверняка не разрешает запросы из браузеров с чужих доменов |
| 💸 Лимиты | 1000 вызовов в день. 5000 посетителей — и лимит сгорел к обеду: ни кеша, ни контроля |
Поэтому схема всегда трёхзвенная:
Браузер ──GET /api/weather/moscow──▶ ТВОЙ сервер ──запрос с ключом──▶ Чужой API
◀──────────JSON──────────── ◀───────JSON────────
ключа не видит ключ живёт только здесьТвой сервер — посредник (gateway): прячет ключ, кеширует ответы, обрабатывает чужие ошибки.
Чужой API — это чужой сервер со своими проблемами. И теперь его проблемы — твои:
ECONNREFUSED) — сервис лежит целиком.ETIMEDOUT) — сеть моргнула или сервис захлебнулся.Хороший посредник умеет три вещи: ретраить случайные сбои, кешировать ответы, деградировать красиво, когда всё совсем плохо. Именно это мы и построим.
| Метод | Путь | Что делает | Статусы |
|---|---|---|---|
GET | /api/weather/:city | Погода одного города | 200 / 404 / 502 |
GET | /api/weather?cities=a,b | Несколько городов одним запросом | 200 / 400 |
В теле ответа: temp, condition, плюс служебные флаги cached (отдано из кеша) и stale (данные устарели, но лучше, чем ничего).
В песочнице нет интернета, поэтому чужой API имитирует функция externalWeatherApi(city) — считай её «fetch на api.meteo.example». Рядом — рычаги, которыми тесты (и превью) ломают чужой сервис:
_setApiMode('down') — уронить API: каждый вызов бросает ECONNREFUSED_setApiMode('flaky', 2) — следующие 2 вызова упадут с ETIMEDOUT, дальше заработаетapiCalls — счётчик обращений: по нему видно, сколько «денег» ты потратилЭто честный приём из реальной разработки: интеграционные тесты всегда мокают внешние сервисы — нельзя же в тестах ронять настоящий OpenWeather. В финальном шаге ты заменишь имитацию настоящим fetch на настоящий погодный API.
Справа — скелет сервера. Блок имитации не редактируй — твой код живёт ниже него. Поехали! 🌦
const app = express();
app.use(express.json());
// ═════ ИМИТАЦИЯ ЧУЖОГО API — не редактируй этот блок ═════
// В реальности здесь был бы fetch('https://api.meteo.example/...').
// В песочнице нет интернета, поэтому чужой сервис — функция с «рычагами поломки».
let weatherDb = { moscow: { temp: -4, condition: 'снег' }, sochi: { temp: 12, condition: 'солнечно' } };
let apiMode = 'ok'; // 'ok' | 'down' | 'flaky'
let apiCalls = 0; // счётчик обращений (каждый вызов — деньги!)
let flakyFailsLeft = 0; // сколько ближайших вызовов упадёт в режиме flaky
function externalWeatherApi(city) {
apiCalls++;
if (apiMode === 'down') throw new Error('ECONNREFUSED');
if (apiMode === 'flaky' && flakyFailsLeft > 0) { flakyFailsLeft--; throw new Error('ETIMEDOUT'); }
var data = weatherDb[city];
if (!data) return null;
return { city: city, temp: data.temp, condition: data.condition };
}
function _setApiMode(mode, fails) { apiMode = mode; flakyFailsLeft = fails || 0; }
function _apiStats() { return { calls: apiCalls, mode: apiMode }; }
// ═════ конец имитации ═════
// Дальше — ТВОЙ сервер. Его ты соберёшь шаг за шагом:
app.get('/api/weather/:city', (req, res) => {
const data = externalWeatherApi(req.params.city); // вызов «чужого» API
if (!data) return res.status(404).json({ error: 'Город не найден' });
res.json({ ...data, cached: false });
});
app.listen(3000, () => console.log('✅ http://localhost:3000'));Основной сценарий «погода одного города». Нажми на шаг — увидишь пояснение и код ответа.
Клиент спрашивает погоду одного города. Город берётся из req.params.city. Наш сервер — посредник (gateway): прячет API-ключ и защищает клиента от чужих сбоев.