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

Kate

Administrator
Команда форума
Цели:

  • Создать 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.

719c9a6ada40e6aac24697f6bb0b9643.jpg

Создание 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

bab110e90a37d8fb97a644310efe8c1f.jpg

Документация 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.

804c803e702b274d682cfd109b6edb6a.jpg

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

96221a2a4fba6619a79d8726595e4fd4.png

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

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

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

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

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

5b5d7b81f49b445adcf3b8ba427aee35.png

Заголовки 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 выглядит так:

444d0e80f386fc7e048734778dad2681.png

Для каждой модели в документации 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:

edf8bedcd0a23a2d255c303402fd6ad4.jpg


 
Сверху