Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 13 из 20 Средний 40 мин 130 XP

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. Смотрим на настоящий эндпоинт:

Первый JSON-эндпоинт (запустите)
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 сортирует ключи по алфавиту.

Посмотрим на сырое тело ответа — те самые байты, которые ушли бы по сети:

Сырое тело JSON-ответа (запустите)
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? Сравним:

Словарь и 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 было бы таблицей базы. Для обучения этого достаточно, а про память между запросами поговорим после полного цикла:

GET-список заметок (запустите)
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() с той разницы, что читает он запрос, а не ответ:

POST создаёт заметку (запустите)
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 — и посмотрим, что накопилось:

API задач: полный цикл (запустите)
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? Проверим:

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 или браузерные инструменты:

app.py целиком и запросы из терминала
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"}
Сетевой сервер в браузерной песочнице не запускается — app.run() здесь не используется. Это реальный вывод curl против того же кода: test_client повторяет такие запросы без сети.

Обратите внимание на тело ответа 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)
Проверь себя
0 / 6

1. Чем API отличается от обычного сайта?

2. Что делает jsonify во Flask?

3. Ключи передали в порядке z, a, m. В каком порядке они уйдут в JSON-ответе jsonify?

4. Какой статус правильнее вернуть после успешного создания ресурса через POST?

5. Как прочитать JSON из тела входящего POST-запроса?

6. Что означает ответ 405 на POST-запрос?

Карточки терминов
Запомнено: 0 / 6
Практика

Достройте книжный API до полного цикла: маршрут /api/books с методами GET и POST. POST читает request.get_json(), создаёт книгу вида {"id": новый номер, "title": название} и возвращает её со статусом 201. GET отдаёт весь список. После объявления сделайте два POST ("Мёртвые души" и "Отцы и дети") и напечатайте итоговый GET-список построчно: id и название.

practice.py
Вопросы и ответы по уроку

Как вернуть 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.

Проверьте знания по Flask

В челлендже — 20 задач по Flask, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.

Тест по Flask: 20 задач