Цели:

  • Создать API с помощью Django REST Framework;

  • Создать динамическую документацию Swagger;

  • Сгенерировать для API код клиента на TypeScript;

  • Создать базовое приложение на ReactJS, которое будет использовать сгенерированный код на TypeScript для отображения данных из API.

Исходный код:

Требования:

В этом руководстве нам понадобятся конкретные пакеты в случае, если вы не будете использовать шаблон проекта Djangitos:

Django==3.2.7
djangorestframework==3.12.4
drf-yasg==1.20.0
django-filter==2.4.0
django-cors-headers==3.8.0

Установка проекта

Скачайте последний шаблон проекта Djangitos, переименуйте папку проекта и скопируйте локальный .env-файл. 

curl -sSL https://appliku.com/djangitos.zip > djangitos.zip
unzip djangitos.zip


mv djangitos-master drfswagger_tutorial
cd drfswagger_tutorial
cp start.env .env

Запустите проект:

docker-compose up

Примените миграции: 

docker-compose run web python manage.py migrate

Создайте аккаунт суперпользователя:

docker-compose run web python manage.py makesuperuser

В выводе после последней команды будут логин и пароль администратора, которого вы только что создали. Нечто подобное:

admin user not found, creating one
===================================
A superuser was created with email admin@example.com and password xLV9i9D7p8bm
===================================

Перейдите на http://0.0.0.0:8060/admin/ и войдите под этими учетными данными.

Создание приложения и моделей:

Давайте создадим приложение, в котором будут наши модели и API.

docker-compose run web python manage.py startapp myapi

Добавьте приложение в PROJECT_APPS в djangito/settings.py:

PROJECT_APPS = [
    'usermodel',
    'ses_sns',
    'myapi',  # new
]

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

У нас есть три модели: Category, Book, Author.

Вот код, который будет в myapi/models.py:

from django.db import models


class Category(models.Model):
    title = models.CharField(max_length=255)

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = 'category'
        verbose_name_plural = 'categories'
        ordering = ('title',)


class Author(models.Model):
    name = models.CharField(max_length=255)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = 'author'
        verbose_name_plural = 'authors'
        ordering = ('name',)


class Book(models.Model):
    title = models.CharField(max_length=255)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    authors = models.ManyToManyField(Author)

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = 'book'
        verbose_name_plural = 'books'
        ordering = ('title',)

    def authors_names(self) -> list:
        return [a.name for a in self.authors.all()]

Для этих моделей нам понадобится интерфейс администратора, поэтому следующий код мы положим в myapi/admin.py:

from django.contrib import admin
from . import models


class CategoryAdmin(admin.ModelAdmin):
    pass


admin.site.register(models.Category, CategoryAdmin)


class AuthorAdmin(admin.ModelAdmin):
    pass


admin.site.register(models.Author, AuthorAdmin)


class BookAdmin(admin.ModelAdmin):
    filter_horizontal = ('authors', )
    list_display = ('title', 'category',)


admin.site.register(models.Book, BookAdmin)

Теперь нам нужно сделать миграции для наших моделей:

docker-compose run web python manage.py makemigrations myapi
docker-compose run web python manage.py migrate myapi

Теперь мы можем перейти к панели администратора и посмотреть на наши модели: http://0.0.0.0:8060/admin/myapi/

Создайте несколько объектов для каждой модели, чтобы мы смогли увидеть результат, когда позже будем играться с API.

Создание API

Во время создания API мы не будем делать аутентификацию и отложим эту часть, чтобы сосредоточить на документации Swagger и клиентской библиотеке на TypeScript.

Сначала давайте создадим сериализаторы для наших моделей.

Создайте файл myapi/serializers.py:

from . import models
from rest_framework import serializers


class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Category
        fields = ('id', 'title',)


class AuthorSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Author
        fields = ('id', 'name',)


class StringListSerializer(serializers.ListSerializer):
    child = serializers.CharField()


class BookSerializer(serializers.ModelSerializer):
    authors_names = StringListSerializer()

    class Meta:
        model = models.Book
        fields = ('id', 'title', 'category', 'authors', 'authors_names',)

Я предпочитаю хранить представление API и представления, которые генерируют HTML, в отдельных файлах.

Создаем файл myapi/api.py:

from rest_framework.generics import ListAPIView
from . import serializers
from . import models


class CategoryListAPIView(ListAPIView):
    serializer_class = serializers.CategorySerializer

    def get_queryset(self):
        return models.Category.objects.all()


class AuthorListAPIView(ListAPIView):
    serializer_class = serializers.CategorySerializer

    def get_queryset(self):
        return models.Author.objects.all()


class BookListAPIView(ListAPIView):
    serializer_class = serializers.BookSerializer

    def get_queryset(self):
        return models.Book.objects.all()

Положите эти URL-адреса для нашего API в файл myapi/urls.py:

from django.urls import path
from . import api

urlpatterns = [
    path('category', api.CategoryListAPIView.as_view(), name='api_categories'),
    path('authors', api.AuthorListAPIView.as_view(), name='api_authors'),
    path('books', api.BookListAPIView.as_view(), name='api_books'),
]

Добавьте URL-адреса в URLConf в djangito/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('api/', include('myapi.urls')),  # new
    path('sns/', include('ses_sns.urls')),
    path('admin/', admin.site.urls),
    path('ckeditor/', include('ckeditor_uploader.urls')),
]

Теперь вы можете открыть эндпоинт books в браузере и посмотреть, как все работает: http://0.0.0.0:8060/api/books

Документация Swagger

Давайте создадим динамическую документацию для нашего API.

Для этого давайте добавим URL-адреса в корневой URLconf в djangito/urls.py:

from django.conf.urls import url
from django.contrib import admin
from django.urls import path, include
from django.views.generic import TemplateView
from drf_yasg.views import get_schema_view  # new
from drf_yasg import openapi  # new
from rest_framework import permissions

schema_view = get_schema_view(  # new
    openapi.Info(
        title="Snippets API",
        default_version='v1',
        description="Test description",
        terms_of_service="https://www.google.com/policies/terms/",
        contact=openapi.Contact(email="contact@snippets.local"),
        license=openapi.License(name="BSD License"),
    ),
    # url=f'{settings.APP_URL}/api/v3/',
    patterns=[path('api/', include('myapi.urls')), ],
    public=True,
    permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
    path(  # new
        'swagger-ui/',
        TemplateView.as_view(
            template_name='swaggerui/swaggerui.html',
            extra_context={'schema_url': 'openapi-schema'}
        ),
        name='swagger-ui'),
    url(  # new
        r'^swagger(?P<format>\.json|\.yaml)$',
        schema_view.without_ui(cache_timeout=0),
        name='schema-json'),
    path('api/', include('myapi.urls')),
    path('sns/', include('ses_sns.urls')),
    path('admin/', admin.site.urls),
    path('ckeditor/', include('ckeditor_uploader.urls')),
]

Мы добавили два импорта и два новых URL-адреса. Один для описания схемы в форматах JSON или YAML, а второй для отображения TemplateView в удобном интерактивном интерфейсе.

Для TemplateView нам понадобится создать шаблон в /templates/swaggerui/swaggerui.html:

<!DOCTYPE html>
<html>
  <head>
    <title>Swagger</title>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="//unpkg.com/swagger-ui-dist@3/swagger-ui.css" />
  </head>
  <body>
    <div id="swagger-ui"></div>
    <script src="//unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
    <script>
    const ui = SwaggerUIBundle({
        url: "{% url "schema-json" ".yaml" %}",
        dom_id: '#swagger-ui',
        presets: [
          SwaggerUIBundle.presets.apis,
          SwaggerUIBundle.SwaggerUIStandalonePreset
        ],
        layout: "BaseLayout",
        requestInterceptor: (request) => {
          request.headers['X-CSRFToken'] = "{{ csrf_token }}"
          return request;
        }
      })
    </script>
  </body>
</html>

Попробуйте открыть документацию в браузере: http://0.0.0.0:8060/swagger-ui/

Теперь вы видите, что все эндпоинты определены и каждый созданный нами сериализатор, который мы использовали в эндпоинтах API, указан в разделе Models.

Давайте подробнее взглянем на эндпоинт books:

Благодаря нашим сериализаторам мы видим ожидаемый тип ответа без вызова эндпоинтов.

Обратите особое внимание на поле authors_names нашего BookSerializer и поле authors_names в ответе эндпоинта.

Мы создали метод authors_names в модели Book, который возвращает список строк, создали дополнительный StringListSerializer и создали поле authors_names в BookSerializer. Нам не нужно указывать many=True для определения этого поля сериализатора, поскольку это уже заложено в ListSerializer и его many=True по умолчанию.

Мы могли бы создать поле лениво, с помощью authors_names = serializers.SerializerMethodField(). Однако в таком случае в документации swagger у нас была бы просто строка для этого поля, что не совсем верно. Старайтесь избегать использования SerializerMethodField, поскольку у вас не будет контроля над сгенерированной документацией.

Последнее, что нужно посмотреть, это то, как сериализаторы описаны в документации в блоке Models:

Заголовки CORS

Чтобы наш клиент смог получить доступ к API нам нужно сконфигурировать django-cors-headers. Добавьте следующие строки кода в файл djangito/settings.py:

CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000"
]

CORS_EXPOSE_HEADERS = ['Content-Type', 'X-CSRFToken']
CORS_ALLOW_CREDENTIALS = True

Создание приложения React и генерация TypeScript API Client из swagger.json

Чтобы показать клиентскую библиотеку, давайте создадим очень простое приложение ReactJS. 

Для начала удалим предыдущую версию create-react-app:

npm uninstall -g create-react-app

А теперь давайте создадим демо-приложение React:

npx create-react-app swagger-api-demo --template typescript
cd swagger-api-demo

Глобально установите OpenAPI Typescript Codegen:

npm install -g openapi-typescript-codegen

И давайте сгенерируем API клиента:

wget http://0.0.0.0:8060/swagger.json -O swagger.json && openapi  --input ./swagger.json --output ./src/api -c fetch

Теперь наш проект React выглядит так: 

Для каждой модели в документации Swagger у нас есть файл TypeScript в папке models. Эндпоинты нашего API представлены сервисами.

Давайте создадим компонент, который мы будем загружать и рендерить нашу книгу.

Создайте файл src/BooksList.tsx в проекте React:

import {useEffect, useState} from "react";
import {Book, BooksService} from "./api";

function BookItem(props: Book) {
    return <div>
        <b>{props.title}</b>
        <i>{props.authors_names.join(', ')}</i>
    </div>;
}

export default function BooksList() {
    const [books, setBooks] = useState<Book[] | undefined>();
    const loadBooks = async () => {
        setBooks(await BooksService.booksList());
    }
    useEffect(() => {
        loadBooks();
    }, []);
    return (
        <div>
            <h1>Books:</h1>
            {books && books.map(
                book => {
                    return <BookItem {...book}/>;
                })}
        </div>
    );
}

А затем замените src/App.tsx следующим кодом: 

import React from 'react';
import './App.css';
import BooksList from "./BooksList";


function App() {
    return (
        <div className="App">
            <BooksList/>
        </div>
    );
}

export default App;

А теперь в корне проекта React выполните эту команду, чтобы запустить dev-сервер:

npm start

После нее должно открыться окно браузера. Если не открылось, перейдите на http://localhost:3000/.

Как видите, наше приложение React успешно загрузило список книг из нашего API:


Материал подготовлен в рамках курса «Web-разработчик на Python». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.

Комментарии (8)


  1. 776166
    13.10.2021 14:09

    Неуд по DevOps.

    Сразу в Докер? Виртуальное окружение? Почему низкие порты для основных служб, зачем это? Зачем вам этот root? Почему такой лютейший пофигизм и несоблюдение стандартов? В продакшене у вас runserver? SRLY?


    1. dimuska139
      13.10.2021 14:15
      +1

      Не обращайте внимание, это ж статьи от OTUS - у них всегда такое качество. Курсы, видимо, такие же по уровню, как и их статьи.


      1. MaxRokatansky Автор
        19.10.2021 13:00

        Вы проходили у нас курсы или делаете выводы по переводу статьи, которая даже не является нашим авторским материалом?

        Приходите на бесплатное занятие и тогда сможете оценить уровень курсов ;-)


        1. dimuska139
          19.10.2021 13:13

          А почему я не могу делать выводы по статьям, которые публикуются в официальном блоге компании OTUS? Это ж своего рода реклама курсов. Ведь ваши специалисты, наверное, проверяют статьи перед публикацией, даже если это просто перевод.


        1. 776166
          19.10.2021 16:50

          Так работает брендирование. Всё, что с связано с одним экземпляром продукции брэнда, распространяется на остальную продукцию. Уровень курсов ясен из данной статьи. И уровень этот невысок.


    1. Spiritschaser
      13.10.2021 23:47
      +1

      А мне понравилось.

      Мне как раз эта информация нужна. С учётом того, что у меня проект на shared хостинге с питоном, я сразу могу на virtenv это переделать, бе безумных npm -g и прочего.
      Но если бы я немного не разбирался в flask и django, статья, наверно, больше навредила бы.


    1. dolfinus
      14.10.2021 09:52
      +2

      Сразу в Докер?

      А что не так? Он удобен для воспроизведения prod среды на девелоперских машинках. Нет зависимости от ОС, не надо страдать с установкой СУБД на хост, и т.п.

      Почему низкие порты для основных служб

      8060 это низкий порт? К тому жеч докер позволяет настроить маппинг портов, какая разница, что там слушает приложение внутри контейнера?


      1. 776166
        14.10.2021 15:03
        +1

        Вы озвучили просто показательный пример того, как наплевательски используют докер, и то, о чём говорю я: «Какая вообще разница, как оно там внутри контейнера работает, если снаружи всё в порядке?»

        Порт 8060 действительно не низкий, root для него не нужен, тут я совсем неправ.
        Но если вы посмотрите в конфиг докера, то там используются высокие порты для баз и чего-то там ещё, которые очевидно поднимаются для этого проекта. В подобных случаях очень удобно брать высокий порт и отсчитывать от него. Особенно это удобно, если подобных установок несколько. Особенно в продакшене. В этом случае у вас по одному номеру будет понятно, что за сервис, и к какому проекту он принадлежит, не будет пересечений и не надо будет каждый раз придумывать нужные порты.