Цели:
Создать 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». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.
776166
Неуд по DevOps.
Сразу в Докер? Виртуальное окружение? Почему низкие порты для основных служб, зачем это? Зачем вам этот root? Почему такой лютейший пофигизм и несоблюдение стандартов? В продакшене у вас runserver? SRLY?
dimuska139
Не обращайте внимание, это ж статьи от OTUS - у них всегда такое качество. Курсы, видимо, такие же по уровню, как и их статьи.
MaxRokatansky Автор
Вы проходили у нас курсы или делаете выводы по переводу статьи, которая даже не является нашим авторским материалом?
Приходите на бесплатное занятие и тогда сможете оценить уровень курсов ;-)
dimuska139
А почему я не могу делать выводы по статьям, которые публикуются в официальном блоге компании OTUS? Это ж своего рода реклама курсов. Ведь ваши специалисты, наверное, проверяют статьи перед публикацией, даже если это просто перевод.
776166
Так работает брендирование. Всё, что с связано с одним экземпляром продукции брэнда, распространяется на остальную продукцию. Уровень курсов ясен из данной статьи. И уровень этот невысок.
Spiritschaser
А мне понравилось.
Мне как раз эта информация нужна. С учётом того, что у меня проект на shared хостинге с питоном, я сразу могу на virtenv это переделать, бе безумных npm -g и прочего.
Но если бы я немного не разбирался в flask и django, статья, наверно, больше навредила бы.
dolfinus
А что не так? Он удобен для воспроизведения prod среды на девелоперских машинках. Нет зависимости от ОС, не надо страдать с установкой СУБД на хост, и т.п.
8060 это низкий порт? К тому жеч докер позволяет настроить маппинг портов, какая разница, что там слушает приложение внутри контейнера?
776166
Вы озвучили просто показательный пример того, как наплевательски используют докер, и то, о чём говорю я: «Какая вообще разница, как оно там внутри контейнера работает, если снаружи всё в порядке?»
Порт 8060 действительно не низкий, root для него не нужен, тут я совсем неправ.
Но если вы посмотрите в конфиг докера, то там используются высокие порты для баз и чего-то там ещё, которые очевидно поднимаются для этого проекта. В подобных случаях очень удобно брать высокий порт и отсчитывать от него. Особенно это удобно, если подобных установок несколько. Особенно в продакшене. В этом случае у вас по одному номеру будет понятно, что за сервис, и к какому проекту он принадлежит, не будет пересечений и не надо будет каждый раз придумывать нужные порты.