Руководство по работе с Django REST Framework, Swagger и созданию клиента TypeScript для API

от автора

Цели:

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


ссылка на оригинал статьи https://habr.com/ru/articles/583220/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *