
Цели:
Создать 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». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.
