In this tutorial:
Objectives
- create an API with Django REST Framework,
- have Swagger dynamic documentation,
- generate TypeScript client code for this API,
- create a basic ReactJS app that uses the generated TypeScript code to display data from our API.
Source code:
- Django project repository: https://github.com/appliku/drfswagger_tutorial
- React project repository: https://github.com/appliku/react-swagger-api-demo
Requirements
For this tutorial we need these specific packages, in case you are not planning to use Djangitos project template:
Django==3.2.7
djangorestframework==3.12.4
drf-yasg==1.20.0
django-filter==2.4.0
django-cors-headers==3.8.0
Project Setup
Download fresh Djangitos project template, rename the project folder and copy local development .env
file.
curl -sSL https://appliku.com/djangitos.zip > djangitos.zip
unzip djangitos.zip
mv djangitos-master drfswagger_tutorial
cd drfswagger_tutorial
cp start.env .env
Run the project with:
docker-compose up
Apply migrations with:
docker-compose run web python manage.py migrate
Create a super user account:
docker-compose run web python manage.py makesuperuser
The output of the last command will display the login and password for the admin user that was created, like this:
admin user not found, creating one
===================================
A superuser was created with email admin@example.com and password xLV9i9D7p8bm
===================================
Open http://0.0.0.0:8060/admin/ and login with these credentials.
Create an app and models
Let's create an app where we'll keep our models and API.
docker-compose run web python manage.py startapp myapi
Include the app in PROJECT_APPS in djangito/settings.py
:
PROJECT_APPS = [
'usermodel',
'ses_sns',
'myapi', # new
]
To illustrate solutions for tricky situations of building API we need several models and we'll use a book catalog as an example.
We'll have 3 models: Category, Book, Author.
This is the code for our new 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()]
For these models we need the admin interface, so this is the code for 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)
Now we need to make migrations for our models.
docker-compose run web python manage.py makemigrations myapi
docker-compose run web python manage.py migrate myapi
Now we can go to admin and see our new models.
http://0.0.0.0:8060/admin/myapi/
Create several objects for every model so we can see results when playing with API later.
Making API
While making API we are not going to work on authentication, and leave this part out to focus on Swagger documentation and TypeScript client library.
First step is creating serializers for our models.
Create a file 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',)
I prefer to keep API view and views that generate HTML in separate python files.
Create a file 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()
Put these URLs for our API in file 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'),
]
Add our URLs to the project's URLConf in 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')),
]
Now you can open a books endpoint in your browser and see that it is work http://0.0.0.0:8060/api/books
Swagger documentation
Let's create dynamic documentation for our API.
In order to do that let's add URLs to our root URLconf in 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')),
]
We have added 2 imports, and 2 new URLs. One for our schema description in JSON or YAML formats, another for a TemplateView
that will display it in human readable and interactive interface.
For our TemplateView
we need to create a template in /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>
Try opening documentation in browser: http://0.0.0.0:8060/swagger-ui/
You will see that all our endpoints are defined and every serializer that we've made and used in API endpoints is listed in Models block.
Let's have a closer look on the "books" endpoint.
Thanks to our serializers, we see the expected response type without even actually calling the endpoint.
Please, pay extra attention to the authors_names
field of our BookSerializer
and authors_names
field in the endpoint response.
We made authors_names
method on the Book
model, that returns a list of strings, created an additional StringListSerializer and created a field in BookSerializer
called authors_names
. Look how we don't need to specify many=True
for when defining this serializer field, since it is already based on ListSerializer and it has many=True
by default.
We could have created this field the lazy way, using authors_names = serializers.SerializerMethodField()
approach. But in this case in swagger documentation we would have just "string" for this field, which is incorrect. So, try to avoid using SerializerMethodField
, because you have no control over generated documentation.
Last thing to look at how are serializers are described in documentation in the "Models" block:
CORS Headers
In order for our client app to access our API we need to configure django-cors-headers
module. Add these lines to the djangito/settings.py
file:
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000"
]
CORS_EXPOSE_HEADERS = ['Content-Type', 'X-CSRFToken']
CORS_ALLOW_CREDENTIALS = True
Create React App and Generate TypeScript API Client from swagger.json
For the demonstration of client library let's create a very simple ReactJS app.
First, let's delete previous version of create-react-app
:
npm uninstall -g create-react-app
Now let's create our demo react app:
npx create-react-app swagger-api-demo --template typescript
cd swagger-api-demo
Install OpenAPI Typescript Codegen globally:
npm install -g openapi-typescript-codegen
And let's generate our client API:
wget http://0.0.0.0:8060/swagger.json -O swagger.json && openapi --input ./swagger.json --output ./src/api -c fetch
Now this is how our react project looks like:
For every models in our Swagger documentation we have a TypeScript file in models. Our API endpoints are represented by services.
Let's make a component that we load and render our book.
Create a file in our React project src/BooksList.tsx
:
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>
);
}
And replace src/App.tsx
with this code:
import React from 'react';
import './App.css';
import BooksList from "./BooksList";
function App() {
return (
<div className="App">
<BooksList/>
</div>
);
}
export default App;
Now in the root of the react project run this command to start development server:
npm start
This should open a new browser window. If not – open http://localhost:3000/
You will see that our React app has successfully loaded the list of books from our API: