JavaRush /Курсы /Go SELF /Дизайн ресурсов: /api/v1/tasks, /api/v1/tasks/{id} и /don...

Дизайн ресурсов: /api/v1/tasks, /api/v1/tasks/{id} и /done

Go SELF
56 уровень , 2 лекция
Открыта

1. Зачем думать ресурсами, а не командами

Когда начинаешь делать HTTP‑API, очень легко скатиться в стиль «удобных команд»: /createTask, /getTask, /markTaskDone. Это похоже на пульт управления: кнопка «Создать», кнопка «Получить», кнопка «Сделать красиво». Проблема в том, что HTTP уже придумал вам кнопки — это методы. А путь должен описывать что за сущность лежит по этому адресу.

Ресурсный подход — это когда вы представляете API как «витрину сущностей». Есть коллекция задач, есть конкретная задача, есть представление состояния «выполнено». Тогда клиенту проще предсказывать API «по форме», а не учить 30 уникальных URL‑команд.

Хорошая новость: вам не нужно знать «всю REST‑философию». Достаточно держать в голове одну простую фразу:

URL отвечает на вопрос «что это?», а метод отвечает «что сделать?»

Мини-сравнение: командный стиль vs ресурсный

Что хотим Командный URL (плохо) Ресурсный URL (нормально)
получить список задач
GET /getTasks
GET /api/v1/tasks
создать задачу
POST /createTask
POST /api/v1/tasks
получить задачу 7
GET /getTask?id=7
GET /api/v1/tasks/7
отметить задачу 7 выполненной
POST /markDone?id=7
POST /api/v1/tasks/7/done

Командный стиль выглядит «понятным» ровно до тех пор, пока API не разрастается. Потом начинается дрейф: createTask, addTask, newTask, makeTask — и внезапно у вас в API появляется лингвистический курс «синонимы английских глаголов».

2. Версия в пути: зачем нужен /api/v1

Версионирование в пути часто кажется бюрократией: «мы же только начали, какие версии?». Но проблема в том, что API почти невозможно не менять. Вы захотите добавить поле, переименовать поле, поменять формат, «чуть‑чуть» изменить поведение ошибок. И вот тут начинается самое интересное: старые клиенты могут продолжать жить месяцами.

Есть железное правило: если клиент уже интегрировался, то ваша «маленькая правка» для него может стать падением в проде в пятницу вечером. А пятница вечером — это священное время, когда прод лучше не трогать вообще.

В мире Go очень хорошо чувствуется важность совместимости: если вы меняете публичный API (функции, типы), вы рискуете сломать чужой код, который на него опирается. Это прямо описывается как проблема совместимости: «нельзя забирать API, иначе программы сломаются». С HTTP‑контрактами та же история, просто «чужая программа» — это клиент вашего API.

Поэтому /api/v1 — это способ честно сказать: «вот правила версии 1». Если когда‑нибудь понадобятся несовместимые изменения, у вас есть вариант /api/v2, и старые клиенты не обязаны страдать.

Небольшой пример на Go: фиксируем префикс версии

package main

const apiV1 = "/api/v1"

func tasksPath() string {
	return apiV1 + "/tasks"
}

Здесь нет магии — просто привычка: все пути строим от единого префикса. Это снижает риск опечаток и помогает держать контракт ровным.

3. Коллекция задач: /api/v1/tasks

Путь /api/v1/tasks — это коллекция задач. Представьте, что это полка в магазине: на ней стоят все товары (задачи). С полкой обычно делают два действия: «посмотреть, что на полке лежит» и «положить новый товар».

Ровно так же и в API: список задач — это запрос к коллекции, создание задачи — это добавление в коллекцию. Методом вы различаете эти действия, и путь при этом остаётся один и тот же.

Здесь важна психологическая штука: когда путь один, а метод меняется, API кажется «формой», а не «зоопарком ссылок». Клиенту легче запомнить /tasks и дальше уже логически догадаться: GET — читаю, POST — создаю.

Пример: описываем endpoint как объект контракта

package main

type Endpoint struct {
	Method string
	Path   string
}

var listTasks = Endpoint{Method: "GET", Path: "/api/v1/tasks"}
var createTask = Endpoint{Method: "POST", Path: "/api/v1/tasks"}

Если вы сейчас подумали: «зачем мне это в Go‑коде, мы же ещё сервер не пишем?», то ответ простой: так вы тренируете мышцу «контракт сначала». Этот же список потом можно будет использовать и в документации, и в тестах, и даже в логах.

Почему tasks во множественном числе

/tasks, а не /task, — потому что коллекция подразумевает множество. Да, это «правило хорошего тона», но оно реально помогает мозгу. Если видите множественное число, вы ожидаете список. Если видите /tasks/7, вы ожидаете один элемент.

4. Одна задача: /api/v1/tasks/{id}

Если /tasks — это полка, то /tasks/{id} — это карточка конкретной задачи. Здесь {id} — это шаблон в документации, а не реальные фигурные скобки в URL.

То есть в контракте вы пишете:

  • /api/v1/tasks/{id}

а в реальном запросе клиент подставляет число:

  • /api/v1/tasks/7

Что такое {id} по смыслу

id — это идентификатор. В рамках нашего учебного домена проще всего договориться, что id — целое число (например, int). Важно именно «договориться и зафиксировать»: клиент должен понимать, что именно он должен подставить в путь.

Если не фиксировать формат, получится хаос: один клиент шлёт UUID, другой шлёт числа, третий шлёт 0007, а четвёртый шлёт "семь" (потому что он оптимист).

Пример: функция, которая материализует путь из {id}

package main

import "fmt"

func taskPath(id int) string {
	return fmt.Sprintf("/api/v1/tasks/%d", id)
}

Такие функции кажутся мелочью, но они дисциплинируют. Когда вы пишете taskPath(7), вы не забудете /api/v1, не перепутаете tasks с task, и не сделаете лишний слэш. А ещё такие функции потом очень приятно использовать в тестах: вы сравниваете строку, а не собираете её руками в каждом месте.

5. Состояние done: поле и путь /done

С done обычно возникает первая взрослая дискуссия про дизайн API. Почему? Потому что done — это вроде бы просто булево поле в задаче (done: true/false), но пользователю хочется отдельную кнопку «Отметить выполненной», а разработчику хочется отдельный endpoint, где всё просто и понятно.

В нашем домене мы хотим поддержать понятный сценарий: «пометить задачу выполненной». И в плане курса выбран пример пути состояния:

  • /api/v1/tasks/{id}/done

Это читается почти как фраза: «done для задачи {id}». Здесь /done выступает как подресурс или «представление состояния». Мы не делаем вид, что done — отдельная сущность уровня /done/{id}. Мы держим его внутри задачи.

Почему /done — это нормально

Мы раньше ругали «глаголы в URL» (/createTask). Но /done — другой случай: это не «команда на сервер», это часть модели ресурса — состояние. Мы как будто говорим: «у задачи есть признак done, и у него есть отдельное место в пути».

Можно думать так: /api/v1/tasks/7/done — это «ручка» состояния выполненности для задачи 7.

Варианты, которые обычно сравнивают

Есть два популярных подхода:

  1. Делать изменение через обновление задачи (например, частичное) и передавать {"done": true} в теле.
  2. Делать отдельный endpoint состояния: /tasks/{id}/done.

В рамках этой лекции мы фиксируем именно дизайн пути /done. А вот точную семантику метода (POST/PATCH) и детали статусов лучше удерживать как часть общего контракта, но без глубокого ухода в реализацию.

Пример: функция пути done для конкретной задачи

package main

import "fmt"

func taskDonePath(id int) string {
	return fmt.Sprintf("/api/v1/tasks/%d/done", id)
}

Теперь у вас есть три стабильных «кирпичика»:

  • tasksPath() — коллекция
  • taskPath(id) — один ресурс
  • taskDonePath(id) — состояние done

Это и есть «скелет» URL‑пространства нашего API.

Небольшая схема: как всё связано

Когда в голове всё это смешивается, помогает простая визуализация. Тут нет никакой магии REST, просто дерево.

flowchart TD
    A["/api/v1/tasks
Коллекция задач"] --> B["/api/v1/tasks/{id}
Одна задача"] B --> C["/api/v1/tasks/{id}/done
Состояние done"]

Дерево показывает важную вещь: /done живёт под задачей, а не рядом с ней. Это снижает риск ошибок дизайна, где done вдруг начинает жить как отдельная сущность без смысла.

6. Консистентность словаря: tasks, done, {id}

Очень часто API портят не сложные баги, а мелкая несогласованность. Один эндпоинт использует /tasks, другой /todo, третий /items. У вас в голове это одно и то же, а клиент — не телепат. Он читает документацию буквально.

Поэтому в учебном домене мы держим один словарь:

  • сущность: tasks
  • идентификатор: id
  • состояние: done
  • версия: /api/v1

Если вам хочется добавить «красоты» и назвать задачи todos, остановитесь и задайте себе вопрос: «а что выиграет клиент?». Обычно ответ: «ничего, кроме лишнего места для ошибки».

Здесь полезна аналогия с именами переменных в Go: если вы в одном месте пишете taskID, а в другом id, а в третьем task_id, то код становится тяжелее читать. URL‑дизайн — это то же самое, только «переменные» читают другие программы.

7. Карта API в Go-коде

Даже до того, как у вас появится реальный HTTP‑сервер, полезно держать контракт в виде структуры данных. Это помогает вам не спорить словами («ну там где‑то /done»), а видеть конкретные строки.

И ещё это уменьшает шанс опечаток. Опечатка в URL — один из самых обидных багов: всё «почти правильно», но 404, и вы час ищете лишнюю букву.

Шаг 1: заведём операции и список маршрутов

package main

type Route struct {
	Method  string
	Pattern string
	Op      string
}

func apiRoutes() []Route {
	return []Route{
		{Method: "GET", Pattern: "/api/v1/tasks", Op: "list_tasks"},
		{Method: "POST", Pattern: "/api/v1/tasks", Op: "create_task"},
		{Method: "GET", Pattern: "/api/v1/tasks/{id}", Op: "get_task"},
		{Method: "POST", Pattern: "/api/v1/tasks/{id}/done", Op: "mark_done"},
	}
}

Обратите внимание: мы используем {id} именно как документационный шаблон. Это удобно для чтения и обсуждения.

Шаг 2: сделаем печать «карты» как мини-документацию

package main

import "fmt"

func printRoutes(routes []Route) {
	for _, r := range routes {
		fmt.Printf("%s %-25s  # %s\n", r.Method, r.Pattern, r.Op)
	}
}

Шаг 3: соберём всё в main

package main

func main() {
	printRoutes(apiRoutes())
}

Если запустить, вывод будет примерно такой:

GET  /api/v1/tasks             # list_tasks
POST /api/v1/tasks             # create_task
GET  /api/v1/tasks/{id}        # get_task
POST /api/v1/tasks/{id}/done   # mark_done

Это выглядит почти как черновик README. И в этом прелесть: контракт должен быть простым, чтобы помещаться «на один экран».

Почему менять пути почти всегда больно

Пути — это часть контракта, и их тяжело менять без последствий. Даже если вы «просто переименовали» /tasks в /todo, клиенты перестанут работать.

В Go‑мире тема совместимости — не абстракция. Если публичный API меняется, чужой код ломается, и это очень практическая проблема: нельзя «забрать API назад» без поломок. HTTP‑контракт ведёт себя так же: вы не можете незаметно «вынуть» endpoint, если на него кто‑то завязан.

И есть ещё один хороший пример того, как совместимость влияет на дизайн: когда в стандартной библиотеке Go добавляли context.Context, нельзя было просто изменить сигнатуру http.Client.Do, потому что это сломало бы кучу кода. Поэтому поддержку контекста встроили в http.Request так, чтобы не ломать старые вызовы.

Это отличный урок: дизайн интерфейса (и API) часто определяется не «идеальной архитектурой», а ответственностью перед пользователями.

8. Типичные ошибки при дизайне путей задач

Ошибка №1: командные URL (/createTask, /markDone) вместо ресурсов.
Обычно это происходит из желания сделать «как в RPC»: отдельная команда на каждое действие. В итоге у вас появляется зоопарк путей, и клиент не может предсказать API по форме. Лечится простым правилом: путь — существительное (ресурс), действие — HTTP‑метод, а исключения вроде /done должны быть частью модели состояния, а не случайной командой.

Ошибка №2: отсутствие единого словаря (/tasks, /todo, /items) и дрейф названий.
Сначала кажется, что это «мелочь». Потом внезапно оказывается, что разные команды разработки называли одно и то же по‑разному, и клиент пишет костыли «если /tasks не работает, попробуй /todo». Нормальный подход — выбрать один домен (tasks) и держать его везде: в путях, в DTO, в документации, в примерах.

Ошибка №3: {id} не определён как формат, и каждый клиент понимает его по‑своему.
Если вы не сказали, что id — это целое число (или UUID), то клиенты начнут угадывать. Особенно весело, когда один клиент шлёт "7", а другой "007", а третий "seven". В контракте нужно явно фиксировать: {id} — это, например, положительное целое число. Даже если вы пока не реализовали проверку, договорённость уже должна существовать.

Ошибка №4: путаница между состоянием и ресурсом для done.
Иногда done внезапно превращают в отдельный верхнеуровневый ресурс (/api/v1/done/{id}), и API становится странным: «где тут задача, а где признак?». В нашем домене done — это состояние задачи, поэтому оно живёт под /tasks/{id}: /tasks/{id}/done. Так клиенту проще понять, что меняется именно состояние конкретной задачи.

Ошибка №5: отсутствие версии (/api/tasks вместо /api/v1/tasks) и надежда “мы всегда будем аккуратными”.
Надежда благородная, но реальность побеждает: API меняется. Без версии вам либо придётся ломать старых клиентов, либо тащить кучу обратной совместимости без явного разделения правил. Версия в пути — простой и честный способ сказать: «вот контракт, за который мы отвечаем».

1
Задача
Go SELF, 56 уровень, 2 лекция
Недоступна
Путь коллекции
Путь коллекции
1
Задача
Go SELF, 56 уровень, 2 лекция
Недоступна
Пути задачи
Пути задачи
1
Задача
Go SELF, 56 уровень, 2 лекция
Недоступна
Карта маршрутов
Карта маршрутов
1
Задача
Go SELF, 56 уровень, 2 лекция
Недоступна
Валидатор URL
Валидатор URL
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ