Через REST API одна программа получает и меняет данные другой по HTTP: у каждого ресурса свой адрес (URL), действие задаёт метод запроса (GET, POST, PUT, PATCH, DELETE), а сервер отвечает кодом статуса и данными, чаще всего в формате JSON. Так мобильные приложения берут с сервера список заказов, а фронтенд сайта сохраняет данные пользователя.
Мы подняли простой REST API на стандартной библиотеке Python 3.14 и отправили к нему запросы через curl 8.22. Вывод в примерах скопирован из этого прогона.
- Что такое REST API простыми словами
- Принципы REST
- Методы REST API
- Пример: простой REST API на Python
- Запросы к REST API через curl
- Как работает REST API: запрос и ответ изнутри
- Идемпотентность на практике
- Коды ответа REST API
- Запросы к API из Python
- Авторизация и безопасность REST API
- Ошибки при проектировании REST API
- REST, SOAP и GraphQL
- Инструменты для тестирования REST API
- Частые вопросы о REST API
- Обязателен ли JSON в REST API?
- Чем REST API отличается от HTTP?
- Когда использовать PUT, а когда PATCH?
Что такое REST API простыми словами
API (application programming interface, программный интерфейс приложения) задаёт правила, по которым одна программа просит у другой данные или действие. Человек работает с приложением через кнопки, а другие программы работают с ним через API: отправляют запрос в оговорённом формате и получают ответ.
REST расшифровывается как Representational State Transfer, по-русски «передача состояния представления». Термин ввёл Рой Филдинг в диссертации 2000 года. REST описывает архитектурный стиль, то есть набор ограничений для распределённой системы, их и называют принципами REST. Это не протокол и не стандарт: спецификации «REST 1.0» нет, а работает REST API поверх обычного HTTP.
Клиент никогда не получает сам ресурс (строку в базе данных, файл), он получает его представление, например JSON. Переходя от ответа к ответу, клиент меняет своё состояние, как человек, который ходит по ссылкам сайта. Отсюда и название. API, который соблюдает ограничения REST, называют RESTful, и в разговоре «REST API» и «RESTful API» означают одно.
Принципы REST
Ограничений шесть, последнее необязательное.
| Принцип | Что требует | Как выглядит на практике |
|---|---|---|
| Клиент-сервер | интерфейс отделён от хранения данных | веб-клиент и мобильное приложение используют один REST API |
| Отсутствие состояния (stateless) | каждый запрос содержит всю информацию для его обработки, сервер не опирается на сохранённый контекст | токен приходит в каждом запросе |
| Кэширование | ответ помечен как кэшируемый или нет | заголовок Cache-Control |
| Единообразный интерфейс (uniform interface) | одни правила для всех ресурсов | одинаковые методы и коды для пользователей, заказов и товаров |
| Многоуровневая система (layered system) | компонент видит только соседний слой | прокси, балансировщик или API gateway между клиентом и сервером |
| Код по требованию | сервер может прислать исполняемый код | JavaScript, который выполняет браузер |
Stateless часто пересказывают как «сервер ничего не хранит». Это неверно: данные (пользователи, заказы) сервер хранит в базе данных. Он не хранит контекст разговора с клиентом, поэтому запрос можно отправить на любой из одинаковых серверов за балансировщиком, а токен клиент шлёт каждый раз.
Единообразный интерфейс складывается из четырёх правил: у ресурса есть адрес; клиент меняет ресурс через представление в теле запроса; сообщение описывает себя само; ссылки в ответе подсказывают следующие действия (HATEOAS). Последнее правило почти никто не выполняет, хотя по исходному определению без гипертекста API нельзя называть REST API. На практике под REST API понимают ресурсы, методы HTTP и JSON. В учебном или внутреннем проекте мы бы на гипермедиа время не тратили: клиенты таких сервисов пишут по документации.

Методы REST API
Метод говорит серверу, что сделать с ресурсом. Пять методов REST API закрывают операции CRUD: создание, чтение, обновление и удаление данных.
| Метод | Действие | CRUD | Безопасный | Идемпотентный |
|---|---|---|---|---|
| GET | получение ресурса или списка | Read | да | да |
| POST | создание нового ресурса | Create | нет | нет |
| PUT | замена ресурса целиком | Update | нет | да |
| PATCH | частичное изменение | Update | нет | не гарантирован |
| DELETE | удаление ресурса | Delete | нет | да |
Безопасный метод только читает данные. Кроме GET, безопасны HEAD (заголовки без тела) и OPTIONS (возможности ресурса). Идемпотентный метод даёт тот же эффект, сколько раз его ни повтори, поэтому такой запрос клиент может повторить автоматически, если связь оборвалась до ответа. POST так повторять нельзя: пользователь может получить два одинаковых заказа.
Тело передают с POST, PUT и PATCH. У тела GET-запроса смысл не определён, часть серверов такой запрос отклоняет, поэтому параметры чтения пишут в URL: /api/users?page=2.

Пример: простой REST API на Python
Сервер работает на Python 3.14 без сторонних библиотек, модуль http.server входит в стандартную поставку. Ресурс один, пользователи: коллекция живёт по адресу /api/users, отдельный пользователь по адресу /api/users/2. Данные хранятся в словаре и после перезапуска пропадают.
# server.py: простой REST API на Python 3.14 без сторонних библиотек
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlsplit
def parse_json(raw):
try:
obj = json.loads(raw)
except ValueError: # битый JSON или не UTF-8
return None
return obj if isinstance(obj, dict) else None
users = {1: {"id": 1, "name": "Ivan", "email": "ivan@example.com"}}
next_id = 2
class UsersAPI(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def reply(self, status, obj=None, **headers):
self.send_response(status)
for name, value in headers.items():
self.send_header(name, value)
body = b"" if status == 204 else json.dumps(obj, ensure_ascii=False).encode()
if status != 204: # у 204 нет тела и нет Content-Length
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def api(self):
global next_id
method = self.command # GET, POST, PUT, PATCH или DELETE
# тело читаем сразу, даже если ответим ошибкой: иначе оно останется в соединении
raw = self.rfile.read(int(self.headers.get("Content-Length", 0)))
path = urlsplit(self.path).path # /api/users?page=2 -> /api/users
parts = path.strip("/").split("/") # ["api", "users", "2"]
if parts[:2] != ["api", "users"] or len(parts) > 3 or (
len(parts) == 3 and not parts[2].isdigit()):
return self.reply(404, {"error": "адрес не найден"})
uid = int(parts[2]) if len(parts) == 3 else None
allowed = "GET, POST" if uid is None else "GET, PUT, PATCH, DELETE"
if method not in allowed.split(", "):
return self.reply(405, {"error": "метод не поддерживается"}, Allow=allowed)
if uid is None and method == "GET":
return self.reply(200, list(users.values()))
if uid is not None and uid not in users:
return self.reply(404, {"error": f"пользователя {uid} нет"})
if method == "GET":
return self.reply(200, users[uid])
if method == "DELETE":
del users[uid]
return self.reply(204)
obj = parse_json(raw)
if obj is None:
return self.reply(400, {"error": "в теле нужен JSON-объект"})
if method == "PATCH": # меняем только переданные поля
users[uid].update({k: v for k, v in obj.items() if k in ("name", "email")})
return self.reply(200, users[uid])
if not obj.get("name"):
return self.reply(422, {"error": "поле name обязательно"})
if method == "POST":
uid, next_id = next_id, next_id + 1
users[uid] = {"id": uid, "name": obj["name"], "email": obj.get("email")}
if method == "POST":
return self.reply(201, users[uid], Location=f"/api/users/{uid}")
return self.reply(200, users[uid]) # PUT заменил пользователя целиком
do_GET = do_POST = do_PUT = do_PATCH = do_DELETE = api
if __name__ == "__main__":
print("REST API: http://127.0.0.1:8000/api/users", flush=True)
HTTPServer(("127.0.0.1", 8000), UsersAPI).serve_forever()
BaseHTTPRequestHandler ищет в классе метод do_ плюс имя HTTP-метода, а мы направляем все пять имён в одну функцию api и берём метод из self.command. Тело api читает до проверок, а путь берёт через urlsplit, чтобы ?page=2 не ломал разбор адреса. Для метода, которого в классе нет (например, HEAD), стандартный обработчик сам ответит 501. Запуск в отдельном окне терминала:
python server.py
REST API: http://127.0.0.1:8000/api/users
except ValueError ловит и битый JSON, и тело не в UTF-8. С одним json.JSONDecodeError запрос с кириллицей из Git Bash на Windows ронял обработчик: Git Bash передал аргумент curl не в UTF-8, и json.loads выбросил UnicodeDecodeError: 'utf-8' codec can't decode byte 0xcc in position 10: invalid continuation byte. Теперь такой запрос получает 400, а кириллицу из Git Bash отправляйте файлом в UTF-8: curl --json @user.json. Маршруты, проверку данных и разбор query-строки в рабочем проекте берёт на себя фреймворк.
Запросы к REST API через curl
Во втором окне терминала пройдём весь CRUD. -X задаёт метод, --json отправляет тело и ставит заголовки Content-Type и Accept: application/json, -w печатает после ответа код статуса. Функция req избавляет от повтора флагов. Команды для bash (Linux, macOS, Git Bash). --json появился в curl 7.82.0, %header{...} в 7.84.0; в старой версии вместо --json пишут -H "Content-Type: application/json" -d.
API=http://127.0.0.1:8000/api/users
req() { curl -s -w '\n%{http_code}\n' "$@"; }
req $API
req "$API?page=2"
curl -s --json '{"name": "Anna", "email": "anna@example.com"}' $API -w '\n%{http_code} Location: %header{location}\n'
req $API/2
req -X PUT --json '{"name": "Anna"}' $API/2
req -X PATCH --json '{"email": "anna@example.org"}' $API/2
req -X DELETE $API/2
[{"id": 1, "name": "Ivan", "email": "ivan@example.com"}]
200
[{"id": 1, "name": "Ivan", "email": "ivan@example.com"}]
200
{"id": 2, "name": "Anna", "email": "anna@example.com"}
201 Location: /api/users/2
{"id": 2, "name": "Anna", "email": "anna@example.com"}
200
{"id": 2, "name": "Anna", "email": null}
200
{"id": 2, "name": "Anna", "email": "anna@example.org"}
200
204
С ?page=2 пришёл тот же список: постраничного вывода в учебном сервере нет. POST вернул 201 Created и заголовок Location с адресом нового пользователя. PUT мы отправили без поля email, и оно стало null: PUT заменяет ресурс целиком, всё, чего нет в теле, пропадает. Чтобы поменять одно поле, нужен PATCH, он вернул пользователя с новой почтой и прежним именем. DELETE ответил 204 без тела.
Как работает REST API: запрос и ответ изнутри
Флаг -v показывает, что curl отправил на сервер (строки с >) и что получил обратно (строки с <):
curl -s -v --json '{"name": "Anna", "email": "anna@example.com"}' $API 2>&1 | grep '^[<>]'
> POST /api/users HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/8.22.0
> Content-Type: application/json
> Accept: application/json
> Content-Length: 45
>
< HTTP/1.1 201 Created
< Server: BaseHTTP/0.6 Python/3.14.3
< Date: Wed, 07 Oct 2026 06:54:26 GMT
< Location: /api/users/3
< Content-Type: application/json; charset=utf-8
< Content-Length: 54
<
Запрос состоит из стартовой строки (метод, путь, версия HTTP), заголовков, пустой строки и тела, которое grep отсёк. Content-Type задаёт формат данных, Content-Length их длину в байтах. Ответ устроен так же, только первой идёт строка статуса. Строка Date у вас будет своя, а новый пользователь получил номер 3: счётчик после удаления второго назад не идёт.

Идемпотентность на практике
Отправим каждый запрос дважды:
req --json '{"name": "Oleg"}' $API
req --json '{"name": "Oleg"}' $API
req -X PUT --json '{"name": "Oleg", "email": "oleg@example.com"}' $API/4
req -X PUT --json '{"name": "Oleg", "email": "oleg@example.com"}' $API/4
req -X DELETE $API/5
req -X DELETE $API/5
{"id": 4, "name": "Oleg", "email": null}
201
{"id": 5, "name": "Oleg", "email": null}
201
{"id": 4, "name": "Oleg", "email": "oleg@example.com"}
200
{"id": 4, "name": "Oleg", "email": "oleg@example.com"}
200
204
{"error": "пользователя 5 нет"}
404
Два одинаковых POST создали двух пользователей, 4 и 5. Два PUT оставили пользователя 4 в одном состоянии. Второй DELETE ответил 404, и идемпотентность это не нарушает: она говорит об эффекте на сервере, а не об одинаковом ответе. После первого и после второго запроса пользователя 5 нет.
Коды ответа REST API
Код статуса трёхзначный, первая цифра задаёт класс: 1xx информационные, 2xx успех, 3xx перенаправление, 4xx ошибка клиента, 5xx ошибка сервера.
| Код | Название | Когда отдавать |
|---|---|---|
| 200 | OK | запрос выполнен, в теле результат |
| 201 | Created | создан ресурс, адрес в Location |
| 204 | No Content | выполнено, тела нет |
| 400 | Bad Request | запрос сломан, например битый JSON |
| 401 | Unauthorized | нет действующих учётных данных, сервер присылает WWW-Authenticate |
| 403 | Forbidden | сервер понял запрос, но отказывает |
| 404 | Not Found | ресурса нет или сервер не раскрывает, что он есть |
| 405 | Method Not Allowed | метод не поддерживается для ресурса, разрешённые в Allow |
| 422 | Unprocessable Content | JSON правильный, но данные не проходят проверку |
| 429 | Too Many Requests | превышен лимит запросов, ожидание может прийти в Retry-After |
| 500 | Internal Server Error | сервер упал на корректном запросе |
Ошибки клиента на нашем сервере:
curl -s -X DELETE $API -w '\n%{http_code} Allow: %header{allow}\n'
req --json '{"email": "x@example.com"}' $API
req --json 'name=Anna' $API
req $API/42
req http://127.0.0.1:8000/api/books
{"error": "метод не поддерживается"}
405 Allow: GET, POST
{"error": "поле name обязательно"}
422
{"error": "в теле нужен JSON-объект"}
400
{"error": "пользователя 42 нет"}
404
{"error": "адрес не найден"}
404
Удалять всю коллекцию сервер не разрешает: 405 и Allow: GET, POST. Тело name=Anna не JSON, это 400. JSON без имени разобран, но не прошёл проверку, это 422. Не отдавайте 200 с {"error": ...} в теле: клиентский код, балансировщик и системы мониторинга смотрят на код статуса, и для них такая ошибка выглядит успешным ответом.

Запросы к API из Python
Для запросов в Python чаще используют библиотеку requests, но её ставят через pip, и без установки скрипт упадёт с ModuleNotFoundError. Клиент ниже обходится стандартным urllib:
# client.py: запросы к REST API из Python без сторонних библиотек
import json
from urllib.error import HTTPError
from urllib.request import Request, urlopen
API = "http://127.0.0.1:8000/api/users"
def call(method, url, obj=None):
body = None if obj is None else json.dumps(obj).encode()
req = Request(url, body, {"Content-Type": "application/json"}, method=method)
try:
with urlopen(req) as resp:
return resp.status, json.loads(resp.read() or "null")
except HTTPError as err: # на 4xx и 5xx urlopen бросает исключение
return err.code, json.loads(err.read())
print(call("POST", API, {"name": "Мария"}))
print(call("GET", API + "/999"))
python client.py
(201, {'id': 6, 'name': 'Мария', 'email': None})
(404, {'error': 'пользователя 999 нет'})
Без try второй вызов упал бы с urllib.error.HTTPError: HTTP Error 404: Not Found. Если в Git Bash кириллица в выводе нечитаема, запускайте PYTHONUTF8=1 python client.py.

Авторизация и безопасность REST API
Раз сервер не хранит сеанс, учётные данные приходят в каждом запросе, обычно токеном в заголовке Authorization: Bearer <токен>. Параметром в URL токен не передают: адрес оседает в истории браузера и в логах веб-сервера. Нет токена или он истёк, сервер отвечает 401 с заголовком WWW-Authenticate; токен верный, но прав на это действие не хватает, ответ 403.
Число из /api/users/42 в SQL подставляют только параметром, иначе сервер открыт для SQL-инъекции. Обмен данными идёт по HTTPS, иначе токен виден в сети. Если REST API вызывает JavaScript со страницы на другом домене, браузер применит политику CORS, и сервер должен разрешить этот домен заголовками.
Ошибки при проектировании REST API
- В адрес пишут глагол:
/getUsers,/deleteUser?id=2. Действие задаёт метод, адрес называет ресурс:DELETE /api/users/2. - Все операции отправляют через POST. Ответы на POST кэшируются только при явных заголовках свежести, а клиент и прокси уже не знают, какой запрос безопасно повторить.
- Версию API не указывают, и несовместимое изменение ломает старые приложения. Версию пишут в путь:
/api/v1/users(в учебном сервере выше её опустили). - Большую коллекцию отдают целиком, хотя её делят на страницы:
?page=2&limit=20.
REST, SOAP и GraphQL
| REST | SOAP | GraphQL | |
|---|---|---|---|
| Что это | архитектурный стиль | протокол обмена сообщениями | язык запросов |
| Формат данных | любой, чаще JSON | XML | любой, чаще JSON |
| Адреса | у каждого ресурса свой URL | адрес сервиса, операции перечислены в его описании | одна точка входа, обычно /graphql |
| Что получает клиент | ресурс в том виде, как его отдаёт сервер | ответ по заранее описанному контракту (обычно WSDL) | ровно те поля, что запросил |
SOAP строже: сообщение упаковано в XML-конверт, контракт описан заранее, транспортом может быть не только HTTP. GraphQL закрывает слабое место REST: для одного экрана приложения иногда нужно несколько запросов с лишними полями, а в GraphQL нужные данные описывают одним запросом. Новый публичный API мы бы начинали с REST: его поймёт любой клиент с curl, а кэширование ответов работает на уровне HTTP.
Инструменты для тестирования REST API
Для ручной проверки хватает curl, в Windows 10 со сборки 17063 он встроен; в Windows PowerShell 5.1 его вызывают как curl.exe, потому что просто curl там означает Invoke-WebRequest. Postman и Insomnia отправляют те же запросы из графического окна. Описание API пишут в формате OpenAPI (бывший Swagger): файл YAML или JSON с адресами, методами и кодами ответа, по которому Swagger UI строит страницу документации. Автотесты REST API проверяют то же, что мы смотрели глазами: код статуса, заголовки Location и Allow, тело ответа. Сервер для них удобно поднимать в контейнере Docker: каждый прогон начинается с одинаковой среды.

Частые вопросы о REST API
Обязателен ли JSON в REST API?
Нет. REST не задаёт формат данных, сервер может отдавать XML, HTML или CSV, а формат указывает заголовок Content-Type. JSON вырос из синтаксиса объектных литералов JavaScript, и браузер разбирает его встроенным JSON.parse без библиотек.
Чем REST API отличается от HTTP?
HTTP это протокол: методы, заголовки, коды. REST описывает, как построить на нём API: ресурсы с адресами, запросы без состояния, единые правила для всех ресурсов. Интерфейс, где всё идёт одним POST на /api, работает по HTTP, но REST API его не назовёшь.
Когда использовать PUT, а когда PATCH?
PUT заменяет ресурс целиком, пропущенные поля обнулятся, как почта в примере выше. PATCH меняет только переданные поля. Если клиент знает весь объект, удобнее PUT: его можно безопасно повторить.








