Маркетингу нужны короткие ссылки для постов и статистика переходов. Строим API: генерация кодов, 302-редирект, счётчик кликов, топ ссылок.
Стартап готовит рекламную кампанию. Маркетологи вставляют в посты ссылки вида:
https://studycode.dev/kursy/backend?utm_source=telegram&utm_medium=social&utm_campaign=summerПроблемы очевидны:
Решение — сокращатель ссылок (как bit.ly или clck.ru). Сервис выдаёт короткий код: short.st/promo1. При переходе — перенаправляет на оригинал и считает клики.
Вот что мы построим — 5 эндпоинтов:
| Метод | Путь | Что делает | Статусы |
|---|---|---|---|
GET | /api/links | Все ссылки | 200 |
POST | /api/links | Создать короткую ссылку | 201 / 400 / 409 |
GET | /:code | Редирект на оригинал + счётчик | 302 / 404 |
GET | /api/links/:code/stats | Статистика по ссылке | 200 / 404 |
GET | /api/links/top?limit=N | Топ ссылок по кликам | 200 |
Когда браузер запрашивает GET /promo1, сервер НЕ отдаёт страницу. Он отвечает:
HTTP/1.1 302 Found
Location: https://studycode.dev/saleБраузер видит 302, читает заголовок Location и сам делает второй запрос — уже по новому адресу. Пользователь ничего не замечает: кликнул на короткую ссылку → оказался на нужной странице. А сервер за эту долю секунды успел записать +1 клик.
В Express это одна строка: res.redirect(url).
В Todo-проекте у задач были id: 1, 2, 3… Здесь так нельзя:
/1, /2, /3 — любой выкачает все ссылки простым циклом. Случайный код a1b2c3 не угадаешь.short.st/promo1 в посте выглядит лучше, чем short.st/8842, — маркетологи смогут брать свои «красивые» коды.А что, если маркетолог просит код promo1, а он уже занят? Формат данных корректный (значит, не 400), но выполнить запрос нельзя — конфликт с текущим состоянием сервера. Для этого есть статус 409 Conflict. Встретим его на шаге про свои коды.
Данные храним в памяти — обычный массив:
let links = [
{ code: 'promo1', url: 'https://studycode.dev/sale', clicks: 12 },
];Справа — скелет всего сервера, чтобы ты видел картину целиком. Запоминать его не нужно: на следующих шагах ты соберёшь каждый маршрут сам и сразу проверишь тестами. Вперёд! 🚀
const app = express();
app.use(express.json());
let links = [
{ code: 'promo1', url: 'https://studycode.dev/sale', clicks: 12 },
{ code: 'docs', url: 'https://expressjs.com', clicks: 30 },
{ code: 'habr', url: 'https://habr.com/article/42', clicks: 5 },
];
// GET — все ссылки
app.get('/api/links', (req, res) => {
res.json(links);
});
// POST — создать короткую ссылку (код случайный или свой)
app.post('/api/links', (req, res) => {
const url = req.body.url;
if (!url || !url.startsWith('http')) {
return res.status(400).json({ error: 'Нужен url, начинающийся с http' });
}
let code = req.body.code;
if (code) {
if (links.find(l => l.code === code)) {
return res.status(409).json({ error: 'Код уже занят' });
}
} else {
code = Math.random().toString(36).slice(2, 8);
}
const link = { code: code, url: url, clicks: 0 };
links.push(link);
res.status(201).json(link);
});
// GET /:code — редирект + счётчик кликов
app.get('/:code', (req, res) => {
const link = links.find(l => l.code === req.params.code);
if (!link) return res.status(404).json({ error: 'Ссылка не найдена' });
link.clicks++;
res.redirect(link.url);
});
// GET — статистика по одной ссылке
app.get('/api/links/:code/stats', (req, res) => {
const link = links.find(l => l.code === req.params.code);
if (!link) return res.status(404).json({ error: 'Ссылка не найдена' });
res.json({ code: link.code, url: link.url, clicks: link.clicks });
});
// GET — топ ссылок по кликам
app.get('/api/links/top', (req, res) => {
const limit = Number(req.query.limit) || 5;
const top = [...links].sort((a, b) => b.clicks - a.clicks).slice(0, limit);
res.json(top);
});
app.listen(3000, () => console.log('✅ http://localhost:3000'));Сокращатель ссылок — нажми на узел, чтобы увидеть пояснение.
Маркетолог присылает длинный URL и, по желанию, красивый код (например promo1). Тело парсит express.json().