Loading...

What is Appliku?

Icon

Simplest way to deploy Python/Django apps

Push code to Git repo, Appliku will build & deploy the app to your cloud servers.

Learn more .

Start Deploying
Icon

Django project template

Django project template that allows you to start building your app, skipping days of fine tuning project settings..

Get Djangitos
Icon

Appliku SaaS Discord Community

The place where you can talk to like minded individuals who are at different stages of building their SaaS or other apps.

Join Community

Django REST Framework Swagger And TypeScript API Client Tutorial

Share post:

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:

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.

Django Admin with List filter search and 3 objects

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

Django REST Framework ListAPIView endpoint

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.

Django REST Framework Swagger UI

Let's have a closer look on the "books" endpoint.

Django REST Framework Swagger UI for an 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:

SwaggerUI Models from Serializers

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:

ReactJS OpenAPI TypeScript Client

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:

ReactJS app loaded and rendered API Response

Image Description Top