$30 off During Our Annual Pro Sale. View Details »

A Documentation-Driven Approach to Building APIs

A Documentation-Driven Approach to Building APIs

Django REST Framework (DRF) does for REST APIs what Django does for web applications more generally. That is, it provides a powerful, flexible framework to help quickly building applications. OpenAPI (formerly Swagger) provides a machine-readable schema for describing the REST APIs made possible by the likes of DRF. When combined, they allow you to do some pretty neat things, which we're going to cover in this talk:

- Automatic generation of schemas from your API
- Validation of APIs using schemas
- Documentation!
- The competition

Let's see if OpenAPI really is the one API schema to rule them all... or something like that.

Stephen Finucane

May 13, 2020
Tweet

More Decks by Stephen Finucane

Other Decks in Technology

Transcript

  1. A DOCUMENTATION-DRIVEN
    APPROACH TO
    BUILDING APIS USING
    DJANGO REST FRAMEWORK
    AND OPENAPI
    Stephen Finucane (@stephenfin)
    Python Ireland Remote MeetUp, May 2020

    View Slide

  2. ABOUT ME
    Senior Software Engineer at Red Hat
    Working on OpenStack since ~2015
    Working on Python for even longer

    View Slide

  3. AGENDA
    Intro to Django and Django REST Framework
    Intro to OpenAPI
    Generating an OpenAPI schema with DRF
    Validating a DRF-based API with OpenAPI
    Documenting Your API
    Wrap Up

    View Slide

  4. Intro to Django and Django REST Framework

    View Slide

  5. View Slide

  6. View Slide

  7. Django is a high-level Python Web framework that
    encourages rapid development and clean, pragmatic
    design. Built by experienced developers, it takes care
    of much of the hassle of Web development, so you
    can focus on writing your app without needing to
    reinvent the wheel. It’s free and open source.

    View Slide

  8. View Slide

  9. Django REST framework is a powerful and flexible
    toolkit for building Web APIs that includes a web
    browsable API, serialization that supports both ORM
    and non-ORM data sources, extensive
    documentation and a large community.

    View Slide

  10. View Slide

  11. $ django-admin startproject mysite
    $ ls
    mysite/
    manage.py
    mysite/
    __init__.py
    settings.py
    urls.py
    asgi.py
    wsgi.py

    View Slide

  12. $ cd myapp
    $ python manage.py startapp core
    $ ls
    core/
    __init__.py
    admin.py
    apps.py
    migrations/
    __init__.py
    models.py
    tests.py
    views.py

    View Slide

  13. class Post(models.Model):
    title = models.CharField(max_length=255)
    date = models.DateTimeField(auto_now_add=True)
    author = models.ForeignKey(
    User, on_delete=models.CASCADE,
    )
    body = models.TextField()
    mysite/core/models.py

    View Slide

  14. class PostSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
    model = Post
    fields = ['url', 'title', 'date', 'author', 'body']
    class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
    model = User
    fields = ['url', 'username', 'email']
    mysite/core/serializers.py

    View Slide

  15. class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [
    permissions.IsAuthenticatedOrReadOnly]
    class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all().order_by('-date_joined')
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticated]
    mysite/core/views.py

    View Slide

  16. router = routers.DefaultRouter()
    router.register(r'users', views.UserViewSet)
    router.register(r'posts', views.PostViewSet)
    urlpatterns = [
    path('', include(router.urls)),
    ]
    mysite/mysite/urls.py

    View Slide

  17. $ python manage.py runserver 0.0.0.0:8000

    View Slide

  18. $ curl -s http://localhost:8000/posts/ | python -m json.tool
    {
    "count": 0,
    "next": null,
    "previous": null,
    "results": []
    }

    View Slide

  19. $ curl -s http://localhost:8000/posts/ | python -m json.tool
    {
    "count": 1,
    "next": null,
    "previous": null,
    "results": [
    {
    "url": "http://localhost:8000/posts/1/",
    "title": "Hello, world",
    "date": "2020-05-13T09:58:54.805866Z",
    "author": "http://localhost:8000/users/1/",
    "body": "This is my first post."
    }
    ]
    }

    View Slide

  20. View Slide

  21. View Slide

  22. Intro to OpenAPI

    View Slide

  23. View Slide

  24. The OpenAPI Specification (OAS) defines a
    standard, language-agnostic interface to RESTful
    APIs which allows both humans and computers to
    discover and understand the capabilities of the
    service without access to source code,
    documentation, or through network traffic
    inspection. When properly defined, a consumer can
    understand and interact with the remote service
    with a minimal amount of implementation logic.

    View Slide

  25. openapi: 3.0.0
    info:
    title: Sample API
    version: 0.1.9
    servers:
    - url: http://api.example.com/v1
    paths:
    /users:
    get:
    summary: Returns a list of users.
    responses:
    '200': # status code
    content:
    application/json:
    schema:
    type: array
    items:
    type: string

    View Slide

  26. Generating an OpenAPI schema with DRF

    View Slide

  27. $ python manage.py generateschema

    View Slide

  28. $ python manage.py generateschema
    openapi: 3.0.2
    info:
    title: ''
    version: ''
    paths:
    /users/:
    get:
    operationId: listUsers
    description: API endpoint that allows users to be viewed or edited.
    parameters:
    - name: page
    required: false
    description: A page number within the paginated result set.
    schema:
    type: integer
    responses:
    # ...

    View Slide

  29. # ...
    responses:
    '200':
    content:
    application/json:
    schema:
    type: object
    properties:
    count:
    type: integer
    example: 123
    next:
    type: string
    nullable: true
    previous:
    type: string
    nullable: true
    # ...

    View Slide

  30. Documenting Your API

    View Slide

  31. urlpatterns = [
    path('', include(router.urls)),
    path('openapi', get_schema_view(
    title='Your Project',
    description='API for all things …',
    version='1.0.0'
    ), name='openapi-schema'),
    path('swagger-ui/', TemplateView.as_view(
    template_name='swagger-ui.html',
    extra_context={'schema_url': 'openapi-schema'}
    ), name='swagger-ui'),
    ]
    mysite/mysite/urls.py

    View Slide

  32. View Slide

  33. View Slide

  34. extensions = [
    'sphinxcontrib.openapi',
    ]
    mysite/docs/conf.py

    View Slide

  35. ============
    Sample API
    ============
    .. openapi:: openapi.yml
    mysite/docs/index.rst

    View Slide

  36. View Slide

  37. extensions = [
    'sphinxcontrib.redoc',
    ]
    redoc = [
    {
    'name': 'Sample API',
    'page': 'api/index',
    'spec': 'openapi.yml',
    'embed': True,
    },
    ]
    mysite/docs/conf.py

    View Slide

  38. ============
    Sample API
    ============
    .. toctree::
    :glob:
    api/*
    mysite/docs/index.rst

    View Slide

  39. View Slide

  40. Validating a DRF-based API with OpenAPI

    View Slide

  41. View Slide

  42. ROOT_DIR = os.path.join(os.path.dirname(__file__), os.pardir)
    SCHEMA = os.path.join(ROOT_DIR, 'docs', 'openapi.yml')
    class SchemaValidationTests(TestCase):
    def test_validate_schema(self):
    with open(SCHEMA) as fh:
    schema = yaml.safe_load(fh)
    openapi_spec_validator.validate_spec(schema)
    mysite/core/tests.py

    View Slide

  43. $ python manage.py test core
    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    .
    --------------------------------------------------------------------
    Ran 1 test in 0.114s
    OK
    Destroying test database for alias 'default'...

    View Slide

  44. View Slide

  45. from django.test.client import RequestFactory
    from openapi_core.validation.request.validators import (
    RequestValidator)
    from openapi_core.contrib.django import DjangoOpenAPIRequest
    request_factory = RequestFactory()
    django_request = request_factory.get('/posts/')
    openapi_request = DjangoOpenAPIRequest(django_request)
    validator = RequestValidator(spec)
    result = validator.validate(openapi_request)
    assert not result.errors, result.errors

    View Slide

  46. View Slide

  47. Wrap Up

    View Slide

  48. WRAP UP
    Use Django + Django REST Framework for fast APIs
    Generate or handwrite OpenAPI schemas
    Use these schemas for documentation
    Use these schemas for validation (server and client)

    View Slide

  49. FURTHER READING
    Transparent validation of requests/responses
    Auto-generation of APIs
    Auto-generation of clients
    ...

    View Slide

  50. A DOCUMENTATION-DRIVEN
    APPROACH TO
    BUILDING APIS USING
    DJANGO REST FRAMEWORK
    AND OPENAPI
    Stephen Finucane (@stephenfin)
    Python Ireland Remote MeetUp, May 2020

    View Slide

  51. Resources
    ● Cover Photo by Alain Pham on Unsplash

    View Slide

  52. References
    ● Getting started (docs.djangoproject.com)
    ● Quickstart (django-rest-framework.org)
    ● Documenting your API (django-rest-framework.org)
    ● openapi-core 0.13.3 (PyPI)

    View Slide