Toast Driven

← Back to November 30, 2023

Released: django-microapi!

I released a small library yesterday, django-microapi. I just wanted to talk a little further about the rationale, what it's good for, and what it's not good for.

Rationale, or Why Another HTTP API Library?

If you know anything about my open-source, you'll likely be aware that I've produced not one, but TWO other RESTful HTTP API frameworks in the past, django-tastypie and restless. Not to mention, I've spent a fair amount of professional time recently using other libraries/frameworks, such as django-rest-framework, FastAPI, and django-ninja.

So why on Earth would I work on & release yet another HTTP API library for Django?

First, some backstory. To be frank, beyond a couple projects, I never really got to use either Tastypie or Restless. They were clearly useful to others, and I spent hundreds of hours supporting other people's use of them. But when you aren't using your own projects & just supporting others, it's easy to lose both enthusiasm and vision. To that end, I tried to bring in other maintainers, including people actually getting use out of them.

I'm not really going to significantly comment on DRF, FastAPI, or Django-Ninja here; beyond saying that I think they're all interesting and fine. I don't like being negative about other people's open-source, and each of their popularity speaks for itself.

The Real Reason

The real reason for django-microapi is that my (desired) approach to building APIs has changed.

  • I build mostly semi-private APIs, where there's only a couple consumers (mostly a Javascript frontend).
  • I don't need many/most of the features these libraries include (e.g. HATEOAS, full PATCH support, OpenAPI, related objects, etc.).
  • I spend too much time fighting opinionated serialization.

And the more I build spent building APIs, the more time I also spend trying to understand complex implementation details & trying to override behavior that doesn't work for me.

So let's stop fighting the frameworks, and write a library to embelish normal Django views instead...

Simple Views

When I started with Django back in 2008, one of the initial draws was how simple/delightful the function-based views were. A couple lines, only what was needed & nothing more, and you were responding to HTTP requests.

from django.shortcuts import render


def hello(request):
    name = request.GET.get("name", "world")
    return render(request, "greeting.html", {
        "name": name,
    })

Fast forward to today, and I longed for that simplicity again, but for APIs. It's slightly more clumsy to do with functions, as you have to support all the different HTTP verbs in a single function.

So we look to class-based views (CBVs), and most of what I want is already present in Django's django.views.generic.base.View, including that delightful simplicity.

But there's a catch. Django's views are built with normal HTML conventions (rendering HTML, form POSTs, Django templates, etc.) in mind. Which are fantastic & flexible! But writing JSON HTTP APIs are more involved, and there's a lot of repetitive boilerplate.

With Plain Views

For example, let's look at a basic RESTful list endpoint using View. GET returns a list of all objects, POST creates a new one.

# Gotta import this everywhere.
import json

# Gotta import this everywhere, & you lose `django.shortcuts`.
from django.http import JsonResponse
# Import a (pretty buried) view.
from django.views.generic.base import View

from myblog.models import BlogPost


class BlogPostListView(View):
    def get(self, request):
        posts = BlogPost.objects.all()
        data = {
            "success": True,
            "posts": [],
        }

        # Handling list serializations can be verbose.
        for post in posts:
            data.append({
                "id": post.id,
                "title": post.title,
                "slug": post.slug,
                "content": post.content,
                "publish_date": post.publish_date,
            })

        return JsonResponse(data)

    def post(self, request):
        # Gotta try/except the load, to return **JSON** error responses!
        # Otherwise, Django will return HTML, which isn't friendly when the
        # client is assuming JSON for everything.
        try:
            # There aren't any JSON shortcuts on `request`, so we have to do
            # this semi-complex invocation all over the place.
            data = json.loads(request.body.read())
        except ValueError:
            return JsonResponse({
                "success": False,
                "errors": [
                    "Invalid JSON payload."
                ],
            })

        # TODO: Ought to validate the data here.

        post = BlogPost.objects.create(
            title=data["title"],
            slug=data["slug"],
            content=data["content"],
            publish_date=data["publish_date"],
        )
        resp_data = {
            "success": True,
            "post": {
                "id": post.id,
                "title": post.title,
                "slug": post.slug,
                "content": post.content,
                "publish_date": post.publish_date,
            },
        }
        # The attribute is `status_code`, but the kwarg name is `status`.
        # Enjoy this disparity when you're writing your tests!
        return JsonResponse(resp_data, status=201)

That's... a non-trivial amount of code if we want to use plain old View. There's also some key omissions, such as: catching all Exceptions & returning JSON errors, centralizing validation/serialization logic, etc. And most of that would have to get duplicated to the next list endpoint, with just a couple lines of business logic changes.

Note: Yes, there are ways to DRY this up and shorten the code. I left things expanded for clarity in what's going on & to show all the concerns. In essence, django-microapi is that DRY layer.

With django-microapi

Now, for how django-microapi shortens/cleans things up. Spoiler alert: It basically just addresses all the repetitive parts that were commented on above.

from microapi import ApiView, ModelSerializer

from .models import BlogPost


class BlogPostView(ApiView):
    def get(self, request):
        posts = BlogPost.objects.all()
        return self.render({
            "success": True,
            "posts": self.serialize_many(posts),
        })

    def post(self, request):
        data = self.read_json(request)

        # TODO: Validate the data here.

        serializer = ModelSerializer()
        post = serializer.from_dict(BlogPost(), data)
        post.save()

        return self.render({
            "success": True,
            "post": self.serialize(post),
        })

All the painful/reptitive code has been streamlined down to the simpler calls. Handling errors the right way is built-in. And a basic/straightforward serializer, while completely optional, is included.

What It's Good For?

  • Small APIs, with a handful of endpoints
  • Mostly-private APIs, where you're both producer & consumer
  • Heavily customized endpoints, where you need deep control over what/how things execute and/or extensively-modified output
  • You want a small, well-documented, easily-understood codebase

What It's Not Good For?

  • If you're producing a large API with many endpoints, it's an unknown quantity at this point. My biggest Real Life™ usage currently sits at ~15-20 endpoints, supporting ~35 verbs.
  • If your API will public and/or have a lot of consumers.
  • If you will be auto-generating clients (& making heavy use of OpenAPI).
  • If your needs are simple, require little customization, and/or you just don't want to spend much time on the API.
  • You want a big community with lots of support.

Conclusion

django-microapi is largely for me & built around my tastes. I'm currently using on it on a long-lived project, where I'm the primary maintainer.

I'm not going to make a big sell-job of it, or say that you should use it. But options are good; and if it helps even one other person, then the time spent releasing/maintaining it will be worthwhile.

Enjoy!

Toast Driven