REST API на Flask: jsonify и методы GET, POST
Тринадцатый урок курса Flask, центральный в расширении: приложение перестаёт отдавать только HTML и начинает говорить JSON. Маршруты с methods, jsonify, статус 201 при создании — и полный цикл POST создал, GET вернул, прямо в браузере.
Редакция Питоники
Десять уроков мы строили сайт для человека: шаблоны, формы, гостевая книга. Но половина запросов в интернете приходит не от людей, а от программ: мобильное приложение читает ленту, сервис доставки спрашивает статус заказа, телеграм-бот дёргает ваш сервер. Им HTML ни к чему — они хотят данные в удобном формате. Формат этот — JSON, а сервис, который его отдаёт, называется API.
Хорошая новость: для API Flask не нужно ничего нового. Те же маршруты, тот же test_client из урока 2. Изменится тело ответа — вместо шаблона JSON, — и появятся правила вежливости: правильные методы GET и POST и честные статусы вроде 201 Created. К концу урока соберём работающий API заметок и прогоним через него полный цикл: создали — прочитали.
jsonify: JSON-ответ одной функцией
JSON (JavaScript Object Notation) — текстовый формат, который выглядит как словарь Python: {"ключ": значение}. Словари, списки, строки, числа, true/false — всё сериализуется почти без потерь. Во Flask для этого есть jsonify. Смотрим на настоящий эндпоинт:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/api/tasks/<int:tid>")
def get_task(tid):
return jsonify({"id": tid, "done": False, "title": "Купить кофе"})
client = app.test_client()
r = client.get("/api/tasks/5")
print(r.status_code)
print(r.content_type)
print(r.get_json())
200
application/json
{'done': False, 'id': 5, 'title': 'Купить кофе'}Три строки вывода — три урока. Статус 200: всё прошло. content_type равен application/json — jsonify сам поставил правильный заголовок, и клиент знает, что перед ним данные, а не страница. Метод r.get_json() разобрал тело обратно в словарь Python. И обратите внимание на порядок ключей: мы передали id первым, а вернулся словарь, начинающийся с done — jsonify сортирует ключи по алфавиту.
Посмотрим на сырое тело ответа — те самые байты, которые ушли бы по сети:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/api/user")
def user():
return jsonify({"name": "Анна", "id": 7, "city": "Казань"})
client = app.test_client()
r = client.get("/api/user")
print(r.data.decode())
{"city":"\u041a\u0430\u0437\u0430\u043d\u044c","id":7,"name":"\u0410\u043d\u043d\u0430"}Два сюрприза разом. Ключи отсортированы: city, id, name. А русские буквы превратились в эскейпы вида \u041a — это код символа «К» в Юникоде. Для JSON это то же самое значение: любой клиент раскодирует их обратно в «Казань». В байтах ответа просто нет ничего, кроме ASCII, — самый совместимый вариант. Если хотите читаемый русский в ответах, это выключается одной настройкой: app.json.ensure_ascii = False после создания приложения.
return dict против jsonify
Современный Flask умеет сериализовать словарь и без jsonify — верните его из view-функции, и будет тот же JSON. Тогда зачем jsonify? Сравним:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/as-dict")
def as_dict():
return {"ok": True}
@app.route("/as-jsonify")
def as_jsonify():
return jsonify({"ok": True})
client = app.test_client()
for url in ("/as-dict", "/as-jsonify"):
r = client.get(url)
print(r.status_code, r.content_type, r.data.decode())
200 application/json {"ok":true}
200 application/json {"ok":true}Ответы одинаковые — даже пустая строка между ними одинаковая: Flask в конце JSON-тела ставит перевод строки, и print добавил свой сверху. Разница в возможностях. jsonify принимает списки напрямую (вернуть jsonify(notes) проще, чем оборачивать словарём), а главное — jsonify можно вернуть с нестандартным статусом и заголовками: jsonify(note), 201. Через полчаса эта строка станет самой важной в уроке.
Хранилище и GET-список
API заметок начнём с чтения. Данные положим в обычный список словарей — то, что в уроке 5 было бы таблицей базы. Для обучения этого достаточно, а про память между запросами поговорим после полного цикла:
from flask import Flask, jsonify
app = Flask(__name__)
notes = [
{"id": 1, "text": "Купить кофе"},
{"id": 2, "text": "Написать API"},
]
@app.route("/api/notes")
def list_notes():
return jsonify(notes)
client = app.test_client()
for n in client.get("/api/notes").get_json():
print(n["id"], n["text"])
1 Купить кофе 2 Написать API
jsonify приняла список — получили массив объектов JSON, ровно то, что ждёт мобильное приложение. Адрес начинается с /api/ — необязательное, но живое соглашение: по префиксу сразу видно, что эндпоинт отдаёт данные, а не страницы.
POST и статус 201 Created
Теперь запись. POST — метод «создай из того, что я принёс»: тело запроса содержит JSON новой заметки. Один маршрут обслуживает оба метода, а внутри мы смотрим на request.method. Принятые данные разбирает request.get_json() — двойник r.get_json() с той разницы, что читает он запрос, а не ответ:
from flask import Flask, jsonify, request
app = Flask(__name__)
notes = [{"id": 1, "text": "Купить кофе"}]
@app.route("/api/notes", methods=["GET", "POST"])
def notes_view():
if request.method == "POST":
data = request.get_json()
note = {"id": notes[-1]["id"] + 1, "text": data["text"]}
notes.append(note)
return jsonify(note), 201
return jsonify(notes)
client = app.test_client()
r = client.post("/api/notes", json={"text": "Позвонить в сервис"})
print(r.status_code)
print(r.get_json())
r = client.get("/api/notes")
for n in r.get_json():
print(n["id"], n["text"])
201
{'id': 2, 'text': 'Позвонить в сервис'}
1 Купить кофе
2 Позвонить в сервисРазбираем по шагам. methods=["GET", "POST"] — без этого списка маршрут принимал бы только GET, а POST получил бы отказ. client.post(..., json={...}) — test_client сам сериализовал словарь и поставил заголовок application/json, как настоящий клиент. request.get_json() вернул словарь из тела запроса. И главное: при создании вернулся статус 201 — «Created». Это не буквоительство: 200 говорит «вот ответ», 201 говорит «ресурс создан, вот он». Клиент по одному числу понимает, что произошло.
| Метод | Смысл | Обычный статус |
|---|---|---|
GET | прочитать данные, ничего не меняя | 200 |
POST | создать новый ресурс | 201 |
PUT / PATCH | обновить существующий ресурс | 200 |
DELETE | удалить ресурс | 200 или 204 |
PUT, PATCH и DELETE строятся по той же схеме, что и POST: метод в methods, request.get_json() для тела, честный статус в ответе. В настоящем проекте список заметок замещает база из урока 5 — механика маршрутов при этом не меняется ни на символ.
Полный цикл: POST создал — GET вернул
Соберём всё в один минимальный API и прогоним сценарий целиком: пустое хранилище, два POST, затем GET — и посмотрим, что накопилось:
from flask import Flask, jsonify, request
app = Flask(__name__)
tasks = []
@app.route("/api/tasks", methods=["GET", "POST"])
def tasks_view():
if request.method == "POST":
data = request.get_json()
task = {"id": len(tasks) + 1, "title": data["title"], "done": False}
tasks.append(task)
return jsonify(task), 201
return jsonify(tasks)
client = app.test_client()
print(client.get("/api/tasks").get_json())
client.post("/api/tasks", json={"title": "Выучить Flask"})
client.post("/api/tasks", json={"title": "Написать API"})
for t in client.get("/api/tasks").get_json():
print(t["id"], t["title"], t["done"])
[] 1 Выучить Flask False 2 Написать API False
Вот он, весь REST в десяти строках. Первый GET вернул пустой список — хранилище чисто. Два POST создали задачи и ответили 201. Финальный GET показал обе: цикл замкнулся. Причём test_client выполняет настоящие запросы без сети: POST создаёт запись, GET её возвращает — полный цикл API за доли секунды. Именно поэтому API удобно разрабатывать и проверять прямо здесь, в браузере.
405: когда метод не разрешён
Что будет, если клиент перепутает метод — пришлёт POST туда, где объявлен только GET? Проверим:
from flask import Flask
app = Flask(__name__)
@app.route("/api/ping")
def ping():
return "pong"
client = app.test_client()
print(client.get("/api/ping").status_code)
print(client.post("/api/ping").status_code)
200 405
Статус 405 Method Not Allowed: маршрут существует, но метод POST для него запрещён. Flask ставит этот отказ сам — ещё один пример принципа из урока 11: API отвечает кодом, а не молчанием.
Как это выглядит настоящим сервером
В песочнице весь урок обошёлся без сервера. На своей машине тот же API запускается знакомой командой flask run, а вместо test_client с ним разговаривает curl или браузерные инструменты:
from flask import Flask, jsonify, request
app = Flask(__name__)
tasks = []
@app.route("/api/tasks", methods=["GET", "POST"])
def tasks_view():
if request.method == "POST":
data = request.get_json()
task = {"id": len(tasks) + 1, "title": data["title"], "done": False}
tasks.append(task)
return jsonify(task), 201
return jsonify(tasks)
# запуск: flask run (файл должен называться app.py)
$ curl http://127.0.0.1:5000/api/tasks
[]
$ curl -X POST -H "Content-Type: application/json" -d '{"title": "Выучить Flask"}' http://127.0.0.1:5000/api/tasks
{"done":false,"id":1,"title":"Выучить Flask"}Обратите внимание на тело ответа curl: done, id, title — ключи снова отсортированы, а кириллица ушла бы в \u-эскейпы. Всё сходится с тем, что вы видели в test_client: клиент другой, поведение API то же. Кстати, test_client проверяет поведение быстрее и повторяемее, чем curl, — поэтому дальше будем тестировать именно им, а в уроке 15 завернём проверки в pytest.
Итоги: приложение заговорило JSON'ом
- API отдаёт JSON вместо HTML: jsonify сериализует данные и ставит заголовок application/json
- jsonify сортирует ключи по алфавиту, а русские буквы по умолчанию уходит в \u-эскейпы — это нормальный JSON
- Словарь можно вернуть и без jsonify, но jsonify нужен для списков и нестандартных статусов
- methods=["GET", "POST"] открывает маршруту оба метода; внутри решает request.method
- POST принимает тело через request.get_json() и отвечает 201 Created
- Лишний метод — отказ 405; хранилище-список в памяти живёт до перезапуска
API работает, но доверчив: пришлите POST без поля title или с телом не-JSON — и он упадёт с 500. Следующий урок лечит это: в уроке 14 добавим валидацию и научимся отвечать на плохие запросы честным 400 вместо падения.
test_client выполняет настоящие запросы без сети: POST создаёт запись, GET её возвращает — полный цикл API за доли секунды.
Сначала предскажи ответ в голове — это главный навык программиста.
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/api")
def api():
return jsonify(b=2, a=1)
client = app.test_client()
print(client.get("/api").data.decode())
from flask import Flask
app = Flask(__name__)
@app.route("/api/ping")
def ping():
return "pong"
client = app.test_client()
print(client.post("/api/ping").status_code)
1. Чем API отличается от обычного сайта?
2. Что делает jsonify во Flask?
3. Ключи передали в порядке z, a, m. В каком порядке они уйдут в JSON-ответе jsonify?
4. Какой статус правильнее вернуть после успешного создания ресурса через POST?
5. Как прочитать JSON из тела входящего POST-запроса?
6. Что означает ответ 405 на POST-запрос?
Достройте книжный API до полного цикла: маршрут /api/books с методами GET и POST. POST читает request.get_json(), создаёт книгу вида {"id": новый номер, "title": название} и возвращает её со статусом 201. GET отдаёт весь список. После объявления сделайте два POST ("Мёртвые души" и "Отцы и дети") и напечатайте итоговый GET-список построчно: id и название.
Как вернуть JSON из Flask-маршрута?
Верните jsonify со словарём или списком: return jsonify({"id": 1}) либо return jsonify(notes). jsonify сам поставит заголовок application/json. Обычный словарь Flask сериализует и без jsonify, но для списков и ответов с нестандартным статусом (return jsonify(note), 201) нужен именно jsonify.
Чем REST API на Flask отличается от обычного сайта на Flask?
Ответами и правилами вежливости: вместо render_template эндпоинты возвращают jsonify, вместо форм данные приходят через request.get_json(), а результаты операций кодируются статусами — 200, 201, 405. Маршруты, тестовый клиент и структура проекта при этом те же самые.
Зачем статус 201, если работает и 200?
Код статуса — машинный контракт: клиент по числу понимает, что произошло, не разбирая тело. 201 Created явно говорит «ресурс создан», на это опираются библиотеки, тесты и мониторинг. Ответ 200 после POST заставляет клиента читать JSON, чтобы понять, создалось ли что-то.
Почему jsonify отсортировал ключи и заменил русские буквы на \u-коды — это ошибка?
Нет, это настройки по умолчанию: сортировка ключей делает ответы воспроизводимыми, а ensure_ascii превращает не-ASCII символы в \u-эскейпы, оставляя тело чистым ASCII. Значения при этом не меняются — клиент раскодирует их обратно. Читаемый вывод включается настройкой app.json.ensure_ascii = False.
Как тестировать Flask API без запуска сервера?
Через app.test_client(): client.get("/api/notes") и client.post("/api/notes", json={...}) выполняют запросы прямо в Python и возвращают ответ со status_code, content_type и get_json(). Полный цикл «POST создал — GET вернул» прогоняется за доли секунды; в уроке 15 такие проверки оформляются тестами pytest.
Понравился урок? Сошлитесь на него
«test_client выполняет настоящие запросы без сети: POST создаёт запись, GET её возвращает — полный цикл API за доли секунды.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
Flask · Урок 2
Первое веб-приложение на Flask: маршруты и режим отладки
Второй урок курса Flask: несколько страниц в одном приложении, динамические маршруты /post/42, конвертеры типов, своя страница 404 и debug=True, который экономит перезапуски.
Flask · Урок 4
Формы в Flask: приём данных от пользователя
Четвёртый урок курса Flask: сайт начинает слушать гостя. HTML-формы, request.form и request.args, серверная валидация, flash-сообщения и защита от повторной отправки формы.
Flask · Урок 14
Валидация в Flask API: плохие запросы и коды 400
Четырнадцатый урок курса Flask: API из урока 13 падает на первом же кривом запросе. Добавляем валидацию — честные 400 с понятным текстом ошибки, 404 для отсутствующих записей и тесты на плохие входы через test_client.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
FastAPI · Урок 1
Что такое API и REST: введение в FastAPI для начинающих
Понять, что такое API, REST, HTTP-методы и JSON — и подготовиться к первому приложению на FastAPI.
rest api для начинающихhttp методы
FastAPI · Урок 5
CRUD на FastAPI: POST, PUT, PATCH и DELETE методы
Строим полный CRUD на FastAPI: POST со статусом 201, PUT с защитой от 404, PATCH и DELETE — и собираем мини-сервис задач целиком.
fastapi crudpost запрос fastapi
Flask · Урок 20
Проект: сервис заметок с тестами — фабрика, blueprint и pytest
Финал расширения: собираем всё выученное в одно приложение — JSON API заметок на фабрике create_app с блюпринтом, дымовой прогон через test_client и полный pytest-набор на 200, 201, 400 и 404.
flask проект api заметок тестыflask api заметок пример
Проверьте знания по Flask
В челлендже — 20 задач по Flask, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.
Тест по Flask: 20 задач