Automatic Rest APIs

Django Rest Framework (DRF) is very powerful and flexible, but it also requires a lot of boilerplate to declare even simple APIs. This is aggravated if we want to build a truly RESTful API with HATEAOS controls (also known as a level 3 API according to Richardson maturity model). This is how REST is supposed to work and, while DRF allow us to do this, it is not the easier path. In Boogie, creating a RESTful API can be as simple as adding a few decorators to your model declarations.

from django.db import models
from boogie.rest import rest_api

@rest_api()
class Book(models.Model):
    author = models.ForeignKey('Author', on_delete=models.CASCADE)
    publisher = models.ForeignKey('Publisher', on_delete=models.CASCADE)
    title = models.TextField()

    def __str__(self):
        return self.title

@rest_api()
class Author(models.Model):
    name = models.TextField()

    def __str__(self):
        return self.name

@rest_api()
class Publisher(models.Model):
    name = models.TextField()

    def __str__(self):
        return self.name

Now, just add the following line on your project’s urls.py:

urlpatterns = [
    ...,
    path('api/', include(rest_api.urls)),
]

Under the hood, Boogie creates Serializer and ViewSet classes for each model using Django REST Framework and configure a router that organizes every end-point declared. Boogie enforces API versioning, so you should point your browser to “/api/v1/” in order to obtain something like this:

{
    "books": "https://my-site.com/api/v1/books/",
    "authors": "https://my-site.com/api/v1/authors/",
    "publishers": "https://my-site.com/api/v1/publishers/"
}

Each resource is then constructed automatically according to the information passed to the rest_api decorator. In our case, it exposes all fields of each model and stores foreign relations as hyperlinks under the “links” object:

{
    "links": {
        "self": "https://my-site.com/api/v1/books/42/",
        "author": "https://my-site.com/api/v1/author/12/",
        "publisher": "https://my-site.com/api/v1/publisher/2/",
    }
    "author": "Malaclypse, The Younger",
    "publisher": "Loompanics Unltd",
    "title": "Principia Discordia"
}

Extending the default API

Extra properties and attributes

We can declare additional attributes using the rest_api.property() decorator.

Additional URLs

By default, Boogie creates two kinds of routes for each resource: one is list-based, usually under /api/v1/<resource-name>/, and the other is a detail view under /api/v1/<resource-name>/<id>/. It is possible to create additional URLs associated with either a single document (detail view) or a queryset (list view).

Those additional urls can be created with the @rest_api.action decorator. We suggest putting those functions in a api.py file inside your app.

# api.py file inside your app

from boogie.rest import rest_api

@rest_api.list_action('books.Book')
def recommended(request, books):
    """
    List of recommended books for user.
    """
    return books.recommended_for_user(request.user)

@rest_api.detail_action('books.Book')
def same_author(book):
    """
    List of authors.
    """
    return book.author.books()

This creates two additional endpoints:

# /api/v1/books/recommended/
[
    {
        "links": { ... },
        "author": "Malaclypse, The Younger",
        "publisher": "Loompanics Unltd",
        "title": "Principia Discordia"
    },
    {
        "links": { ... },
        "author": "Robert Anton Wilson",
        "publisher": "Dell Publishing",
        "title": "Illuminatus!"
    }
]


# /api/v1/books/42/same-author/
[
    {
        "links": { ... },
        "author": "Malaclypse, The Younger",
        "publisher": "Loompanics Unltd",
        "title": "Principia Discordia"
    },
]

Boogie tries to be flexible regarding the input and output parameters of action functions. Generally speaking, everything that can be safely serialized by the rest_api object can be returned as the output of those functions. See the RestAPI.detail_action() documentation for more details.

Custom viewsets and serializers

You can also completely override the default Boogie viewsets and serializers and specify your own classes. The RestAPI.register_viewset() method allow us to completely specify a custom viewset class.

Custom routers

#TODO

Customizing viewsets and serializers

Sometimes, the created viewsets and serializers are not good enough to specify your desired API. Boogie allow us to register completely custom viewset classes, but most this is an overkill: Boogie provides hooks to register special methods to be inserted in Boogie serializer and viewset classes classes so you can still benefit from what Boogie provides by default while having great flexbility.

Object creation hooks

This is common pattern when designing an API: a model have a few hidden fields that are not exposed, but during object creation, they can be calculated from the user that makes the request. The most common use case is probably when we want to add a reference to the user who made the request in an “author” or “owner” field.

Hooks can be registered using any of the decorators RestAPI.save_hook(), RestAPI.delete_hook().

Example:

@rest_api.save_hook(Book)
def save_book(request, book):
    if book.author is None:
        book.author = request.user.as_author()
    return book


@rest_api.save_hook(Book)
def delete_book(request, book):
    if book.can_delete(request.user):
        book.delete()
    else:
        raise PermissionError('user cannot delete book')

Configurations

Boogie understands the following global configurations in Django settings:

BOOGIE_REST_API_SCHEMA:
When not given, it uses the same uri schema (e.g., http) as the current request object. It is possible to override this behavior to select an specific schema such as ‘http’ or ‘https’. This configuration may be necessary when Django is running behind a reverse proxy such as Ngnix. Communication with the reverse proxy is typically done without encryption, even when the public facing site uses https. Setting BOOGIE_REST_API_SCHEMA='https' makes all urls references provided by the API to use https independently of how the user accessed the API endpoint.

Mixin hooks

For maximum flexibility, you can specify an entire mixin class to be included into the inheritance chain during creation. This advanced feature requires knowledge of the inner works of DRF and, to some extent, of Boogie serializer RestAPISerializer and viewset RestAPIBaseViewSet classes. That said, mixin classes can be added to the class using the RestAPI.serializer_mixin() and RestAPI.viewset_mixin() decorators:

@rest_api.viewset_mixin(Book)
class BookViewSetMixin:
    def create(request):
        if request.user.can_register_book():
            return super().create(request)
        else:
            raise PermissionError('user cannot register book!')

Retrieving viewsets and serializers

Boogie exposes the serializers, viewsets and router objects created internally by the rest_api object. They also can be used to directly serialize an object or queryset or to expose a view function.

The easier way to use Boogie serializers is the invoking RestAPI.serialize() method.

Versions

Django Boogie assumes that the API is versioned and can expose different set of resources and different properties of the same resource. By default, all entry points are created under the “v1” namespace. Users can register different fields, properties and actions under different API version names:

@rest_api(['author', 'title'], version='v1')
@rest_api(['title'], version='v2')
class Book(models.Model):
    author = models.CharField(...)
    title = models.Charfield(...)

Other decorators also accept the version argument. Omitting version means that the property is applied to all versions of the API. Versions can also be lists, meaning that the decorator applies the given settings to all versions on the list.

@rest_api.list_action(Book, version=['v1', 'v2'])
def readers(request):
    return book.readers.all()

API Documentation

The rest_api object is a globally available instance of the RestAPI class.

class boogie.rest.RestAPI[source]

Base class that stores global information for building an REST API with DRF.

action(model, func=None, *, version=None, name=None, **kwargs)[source]

Base implementation of both detail_action and list_action.

Please use one of those specific methods.

delete_hook(model, func=None, *, version='v1')[source]

Decorator that registers a hook that is executed before a new object is about to be deleted.

Deletion can be prevented either by raising an exception (which will generate an error response) or silently by not calling the .delete() method of a model or queryset.

Parameters:
  • model – The model name.
  • version – API version. If omitted, it will be included in all API versions.

Examples

@rest_api.delete_hook(Book)
def delete_book(request, book):
    if book.user_can_remove(request.user):
        book.delete()
    else:
        raise PermissionError('user cannot delete book!')
detail_action(model, func=None, **kwargs)[source]

Register function as an action for a detail view of a resource.

Decorator that register a function as an action to the provided model.

Parameters:
  • model – A Django model or a string with <app_label>.<model_name>.
  • func

    The function that implements the action. It is a function that receives a model instance and return a response. RestAPI understands the following objects:

    • Django and DRF Response objects
    • A JSON data structure
    • An instance or queryset of a model that can be serialized by the current API (it will serialize to JSON and return this value)

    Exceptions are also converted to meaningful responses of the form {"error": true, "message": <msg>, "error_code": <code>}. It understands the following exception classes:

    • PermissionError: error_code = 403
    • ObjectNotFound: error_code = 404
    • ValidationError: error_code = 400

    The handler function can optionally receive a “request” as first argument. RestAPI inspects function argument names to discover which form to call. This strategy may fail if your function uses decorators or other signature changing modifiers.

  • version – Optional API version name.
  • name – The action name. It is normally derived from the action function by simply replacing underscores by dashes in the function name.

Usage:

@rest_api.detail_action('auth.User')
def books(user):
    return user.user.books.all()

This creates a new endpoint /users/<id>/books/ that displays all books for the given user.

get_api_info(version='v1', create=False)[source]

Return the ApiInfo instance associated with the given API version.

If version does not exist and create=True, it creates a new empty ApiInfo object.

Returns an ApiInfo instance.

Return the hyperlink of the given object in the API.

get_resource_info(model, version='v1')[source]

Return the resource info object associated with the given model. If version does not exist, create a new ApiInfo object for the given version.

Parameters:
  • model – A model class or a string in the form of ‘app_label.model_name’
  • version – Version string or None for the default api constructor.
Returns:

A ResourceInfo instance.

get_router(version='v1')[source]

Gets a DRF router object for the given API version.

Parameters:version – An API version string.
get_serializer(model, version='v1')[source]

Return the serializer class for the given model.

get_urlpatterns(version='v1')[source]

Return a list of urls to be included in Django’s urlpatterns:

Usage:
urlpatterns = [
    ...,
    path('api/v1/', include(rest_api.get_urlpatterns('v1')))
]

See also

get_router()

get_viewset(model, version='v1')[source]

Return the viewset class for the given model.

Decorator that declares a function to compute a link included into the “links” section of the serialized model.

Parameters:
  • model – The model name.
  • version – API version. If omitted, it will be included in all API versions.
list_action(model, func=None, **kwargs)[source]

Similar to :method:`detail_action`, but creates an endpoint associated with a list of objects.

Usage:

@rest_api.detail_action('auth.User')
def books():
    return Book.objects.filter(author__in=users)

The new endpoint is created under /users/books/

See also

detail_action()

property(model, func=None, *, version='v1', name=None)[source]

Decorator that declares a read-only API property.

Parameters:
  • model – The model name.
  • version – API version. If omitted, it will be included in all API versions.
query_hook(model, func=None, *, version='v1')[source]

Decorator that registers a hook that is executed to extract the queryset used by the viewset class.

Parameters:
  • model – The model name.
  • version – API version. If omitted, it will be included in all API versions.

Examples

@rest_api.query_hook(Book)
def query_hook(request, qs):
    return qs.all()
register(model, fields=None, *, version=None, inline=False, **kwargs)[source]

Register class with the given meta data.

Parameters:
  • model – A Django model
  • version – Optional API version string (e.g., ‘v1’). If not given, it will register a resource to all API versions.
  • fields – The list of fields used in the API. If not given, uses all fields.
  • exclude – A list of fields that should be excluded.
  • base_url – The base url address in which the resource is mounted. Defaults to a dashed case plural form of the model name.
  • base_name – Base name for the router urls. Router will append suffixes such as <base_name>-detail or <base_name>-list. Defaults to a dashed case plural form of the model name.
  • inline – Inline models are not directly part of an API, but can be embedded into other resources.
Returns:

An ResourceInfo object.

register_viewset(viewset=None, base_url=None, *, version='v1', model=None, skip_serializer=False)[source]

Register a viewset class responsible for handling the given url.

If a ModelViewSet is given, the viewset is automatically associated with a model and registered. Can be used as a decorator if the viewset argument is omitted.

Parameters:
  • viewset – Viewset subclass.
  • base_url – Base url under which the viewset will be mounted. RestAPI can infer this URL from the model, when possible.
  • version – API version name.
  • model – Model associated with the viewset, when applicable.
  • skip_serializer – If True, do not register serializer of ModelViewSet subclasses.
save_hook(model, func=None, *, version='v1')[source]

Decorator that registers a hook that is executed when a new object is about to be saved. This occurs both during object creation and when it is updated. The provided function receives a request and an unsaved instance as arguments and must save the instance to the database and return it.

Parameters:
  • model – The model name.
  • version – API version. If omitted, it will be included in all API versions.

Examples

@rest_api.save_hook(Book)
def save_book(request, book):
    book.save()  # Don't forget saving the instance!
    book.owner = request.user
    return book
serialize(obj, request=None, version='v1')[source]

Serialize object and return the corresponding JSON structure.