HTTP-методы и динамические эндпоинты

Суффикс в имени файла задаёт HTTP-метод, скобки — динамический параметр: products.post.ts ловит POST, [id].get.ts — GET по идентификатору.
Суть: имя файла обработчика можно суффиксовать .get, .post, .put, .delete — он сработает только на этот метод. Динамические сегменты [id] читаются через getRouterParam, тело POST/PUT — через readBody. Так строится полноценный REST-эндпоинт.

Реальный API различает методы: GET читает, POST создаёт, PUT обновляет, DELETE удаляет. Nuxt кодирует метод прямо в имени файла через суффикс. server/api/products.get.ts сработает только на GET, server/api/products.post.ts — только на POST. Один и тот же путь /api/products обслуживается разными файлами в зависимости от метода.

// server/api/products.get.ts — список
export default defineEventHandler(() => {
  return getAllProducts()
})

// server/api/products.post.ts — создание
export default defineEventHandler(async (event) => {
  const body = await readBody(event)        // тело запроса
  return createProduct(body)
})

Динамические эндпоинты работают как в страницах — через квадратные скобки. server/api/products/[id].get.ts матчит /api/products/42, а параметр читается хелпером getRouterParam:

// server/api/products/[id].get.ts
export default defineEventHandler((event) => {
  const id = getRouterParam(event, "id")    // "42"
  const product = findProduct(id)
  if (!product) {
    throw createError({ statusCode: 404, statusMessage: "Нет товара" })
  }
  return product
})
   REST на файлах server/api

   GET    /api/products       -> products.get.ts
   POST   /api/products       -> products.post.ts
   GET    /api/products/42    -> products/[id].get.ts
   DELETE /api/products/42    -> products/[id].delete.ts

   тело -> readBody(event)
   параметр -> getRouterParam(event, "id")

Серверные маршруты Nuxt снимают целый класс задач, ради которых раньше поднимали отдельный бэкенд. Прокси к стороннему API, чтобы спрятать ключ; вебхуки от платёжных систем; генерация PDF или OG-картинок; работа с базой данных; отправка писем — всё это живёт в server/ рядом с фронтендом, в одном репозитории и одном деплое. Для небольших и средних проектов это огромное упрощение: не нужно поднимать, версионировать и стыковать два отдельных сервиса. Nuxt становится единой кодовой базой, где фронтенд и бэкенд говорят на одном языке и собираются вместе.

Как работает под капотом

Nitro строит таблицу маршрутов, где ключ — комбинация метода и пути. При запросе он находит обработчик по методу и шаблону, извлекает параметры из URL и складывает их в event.context.params (откуда их и берёт getRouterParam). Хелперы h3 — readBody, getQuery, getRouterParam — это удобные обёртки над разбором запроса. На неподходящий метод вернётся 405, на несуществующий путь — 404.

Смоделируем диспетчеризацию по методу и пути с извлечением параметра:

// Роутер по (метод + путь) с динамическим [id].
const routes = [
  { method: "GET",    pattern: "/api/products",       fn: () => ["A", "B"] },
  { method: "POST",   pattern: "/api/products",       fn: (e) => "создан: " + e.body.name },
  { method: "GET",    pattern: "/api/products/:id",   fn: (e) => "товар " + e.params.id },
  { method: "DELETE", pattern: "/api/products/:id",   fn: (e) => "удалён " + e.params.id },
];

function dispatch(method, path, event = {}) {
  for (const r of routes) {
    if (r.method !== method) continue;
    const rp = r.pattern.split("/"), up = path.split("/");
    if (rp.length !== up.length) continue;
    const params = {};
    let ok = true;
    for (let i = 0; i < rp.length; i++) {
      if (rp[i].startsWith(":")) params[rp[i].slice(1)] = up[i];
      else if (rp[i] !== up[i]) { ok = false; break; }
    }
    if (ok) return r.fn({ ...event, params });
  }
  return "404";
}

console.log(dispatch("GET",    "/api/products"));
console.log(dispatch("POST",   "/api/products", { body: { name: "Книга" } }));
console.log(dispatch("GET",    "/api/products/42"));
console.log(dispatch("DELETE", "/api/products/7"));

Попробуй сам ▶ — обработчик выбирается по паре «метод + путь», а :id вытаскивается в параметры. Это упрощённая логика серверного роутера Nitro.

Хороший дизайн API — половина успеха фулстек-приложения. Придерживайтесь предсказуемых соглашений: существительные во множественном числе для коллекций (/api/products), идентификатор в пути для одной сущности (/api/products/42), а метод выражает намерение. GET ничего не меняет и безопасен для повтора; POST создаёт; PUT или PATCH обновляют; DELETE удаляет. Такая дисциплина делает API самодокументируемым: по одному взгляду на путь и метод понятно, что произойдёт.

Частые ошибки

  • Один файл на все методы. Лучше разнести .get/.post по файлам — чище и явнее.
  • Читать тело синхронно. readBody асинхронный — нужен await.
  • Не валидировать вход. Тело и параметры приходят от клиента — проверяйте их перед использованием.

Best practices

  • Разделяйте обработчики по методам через суффиксы — это самодокументируемый REST.
  • Параметры — через getRouterParam, тело — через await readBody, query — через getQuery.
  • Несуществующие сущности — createError({ statusCode: 404 }); неверный вход — 400.

Итог: суффиксы методов и динамические сегменты превращают server/api в полноценный REST. Хелперы h3 достают тело и параметры. Дальше — серверное middleware и безопасность эндпоинтов.

Проверьте себя
1. Какой файл обработает POST-запрос на /api/products?
Aserver/api/products.ts
Bserver/api/products.post.ts
Cserver/api/post/products.ts
Dserver/api/products.get.ts
2. Как в серверном обработчике прочитать тело POST-запроса?
Aevent.body напрямую
BЧерез await readBody(event)
CЧерез getRouterParam(event)
DТело недоступно на сервере