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



    AND OPENAPI Stephen Finucane (@stephenfin) Python Ireland Remote MeetUp, May 2020
  2. ABOUT ME Senior Software Engineer at Red Hat Working on

    OpenStack since ~2015 Working on Python for even longer
  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
  4. Intro to Django and Django REST Framework

  5. None
  6. None
  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.
  8. None
  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.
  10. None
  11. $ django-admin startproject mysite $ ls mysite/ manage.py mysite/ __init__.py

    settings.py urls.py asgi.py wsgi.py
  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
  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
  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
  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
  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
  17. $ python manage.py runserver

  18. $ curl -s http://localhost:8000/posts/ | python -m json.tool { "count":

    0, "next": null, "previous": null, "results": [] }
  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." } ] }
  20. None
  21. None
  22. Intro to OpenAPI

  23. None
  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.
  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
  26. Generating an OpenAPI schema with DRF

  27. $ python manage.py generateschema

  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: # ...
  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 # ...
  30. Documenting Your API

  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
  32. None
  33. None
  34. extensions = [ 'sphinxcontrib.openapi', ] mysite/docs/conf.py

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

  36. None
  37. extensions = [ 'sphinxcontrib.redoc', ] redoc = [ { 'name':

    'Sample API', 'page': 'api/index', 'spec': 'openapi.yml', 'embed': True, }, ] mysite/docs/conf.py
  38. ============ Sample API ============ .. toctree:: :glob: api/* mysite/docs/index.rst

  39. None
  40. Validating a DRF-based API with OpenAPI

  41. None
  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
  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'...
  44. None
  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
  46. None
  47. Wrap Up

  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)
  49. FURTHER READING Transparent validation of requests/responses Auto-generation of APIs Auto-generation

    of clients ...

    AND OPENAPI Stephen Finucane (@stephenfin) Python Ireland Remote MeetUp, May 2020
  51. Resources • Cover Photo by Alain Pham on Unsplash

  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)