Цели:
В этом руководстве нам понадобятся конкретные пакеты в случае, если вы не будете использовать шаблон проекта 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:
- Создать API с помощью Django REST Framework;
- Создать динамическую документацию Swagger;
- Сгенерировать для API код клиента на TypeScript;
- Создать базовое приложение на ReactJS, которое будет использовать сгенерированный код на TypeScript для отображения данных из API.
- Репозиторий с проектом Django: https://github.com/appliku/drfswagger_tutorial
- Репозиторий с проектом React: https://github.com/appliku/react-swagger-api-demo
В этом руководстве нам понадобятся конкретные пакеты в случае, если вы не будете использовать шаблон проекта 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:
Руководство по работе с Django REST Framework, Swagger и созданию клиента TypeScript для API
Цели: Создать API с помощью Django REST Framework; Создать динамическую документацию Swagger; Сгенерировать для API код клиента на TypeScript; Создать базовое приложение на ReactJS, которое будет...
habr.com