Build reliable Django frontend APIs like a Ninja

11 July 2024

A while back I wrote an article on Integrating Django with React which got some traction. Each time our team starts a new project, we evaluate our architecture and the choices we made in previous projects. This time we decided to try out a few new things to address some of the pain points with frontend JSON APIs.

The biggest problem we had was that there were three sources of truth for the interface:

  • what the backend promised
  • what the API stated
  • what the frontend expected

Ideally, these should all be the same but in practice were different. This was due in part because the API documentation was created manually and separately from the backend implementation. So while we made every effort to make them in sync, sometimes things fell between the cracks. For example, a property changing from being required to not required.

Our goal with this implementation was to create a better API, i.e. one that guaranteed equality between what the backend provided and what the frontend consumed. We also wanted the API to be automatically generated and validated. As a cherry on top, we decided to have the whole application strictly typed.

There is a repository with example code, which you can check out here, and with that out of the way, let’s get into it.

How did we build a better interface

We made it strictly typed, everywhere.

Our backend and frontend communicate through a JSON API, which is awesome, but it does have a downside. To see this downside, let's use a simple example.

Imagine you have these two JSON objects

1: {“value”: 1}
2: {“value”: “1”}

The value type in example 1 is a number type while in example 2 it’s a string. The question is how does the frontend know? Of course, we can always do type checks, but that would be tedious and if we miss one we could end up losing a fight with JavaScript’s type coercion and get garbage results. For example, your API returns two values as numbers, and in your code, you calculate their sum. You would rightly expect 1 + 1 = 2. But if even one of those values changes to be a string in the API and you did not type check, then suddenly 1 + 1 = 11.

On the backend we used mypy to get static typing. To achieve the same on the frontend we switched to TypeScript.

We ensured that promises and expectations between the backend and frontend were aligned.

How?

Well first, we needed a way to have one source of truth for the API documentation. We chose the backend views for this and automatically generated an interface for the frontend.

We went for automatic generation of the Open API documentation from the backend implementation. The frontend interface in turn would be automatically built from those API docs.

Since we were now strictly typed, any changes to the interface from the backend would be flagged on the frontend at build time, avoiding nasty runtime surprises.

The Backend Implementation

We decided to use Django Ninja to develop our API. We chose it because it’s simple to use, and generates the API docs for us. In keeping with tradition, here is some sample code.

In a Django app hello we define a new router with the add API endpoint, that adds two integers:

django-ninja endpoint implemenation

Then in the projects urls.py we bootrap the URLs of the hello router:

django-ninja implemenation urls.py

Simple, right?

Django Ninja takes over the type checking, serialization, and deserialization. But most relevant for us here, it generates an API docs endpoint /api/docs, which looks like this:

Screenshot API docs

And if we update our API, the docs automatically update.

The API will get a little more complex as the project grows, but the base idea is that simple.

On the Frontend

On the frontend, we used the created API JSON file to generate the interfaces and some helper files that simplified the calling of the API.

As said before we used Oazapfts, but there are other npm packages that can generate code from API JSON files, such as openapi-typescript.

All we needed to do was run the command:

oazapfts api_docs.json generated-api.ts

And it created a Typescript file with all the type definitions and functions that the rest of the application could use to call the API. Here is a sample of that, based on the API above.

TypeScript call endpoint implemenation

Because we switched to TypeScript we had errors if the props were wrong, the request object didn’t meet what was required, or if the API changed.

Usage was equally painless because the helper functions were pretty clear and if questions arose there was a handy URL with the API defined, schema, and helpful demos.

One very annoying quirk we ran into, however, was how Django-ninja builds API operation IDs by default. When Oazapfts generates the interface, it uses them for the function names. This is how you can end up with some super loooooong names, not even kidding, here’s an example:

finalsApiMakeUpRegistrationEndpointsGetMakeUpRegistration

Yes, that’s a function name.

It wasn't that big a problem since ozapfts can handle super long names, which shouldn't be a surprise because the authors are German and our German friends really do love their long words. Seriously, call it an egg cracker, not Eierschalensollbruchstellenverursacher!

Thankfully this problem can be avoided entirely because django-ninja lets us customize operation id generation very easily.

What did we learn?

I am a frontend developer and the initial push to TypeScript wasn’t something I was excited for. My initial thought, which my fellow frontend developer shared, was that TypeScript reduced the readability of the code and made beautiful JavaScript code look hideous.

Here’s the backend team’s actual reaction to the thought of Beautiful JavaScript.

That said, the benefits of TypeScript became more apparent as the code-base grew and the additional information on existing code started to help more and more.

Generating Interfaces for the frontend from the API made changes less of a headache. API look-up is always nice, and spending less time on boiler code was incredible. Automated type checking in the CI, ensured that any changes, as little as a single field, are detected immediately.

Writing frontend tests was also easier, as the helper functions were well-defined and made it easy to mock and test every possible outcome.

The frontend team is still split, I became a believer in TypeScript while my colleague is still a skeptic, and is not entirely convinced the benefits were worth the hustle.

On the backend, there wasn’t too much of a change from the normal way of developing Django projects. Django-ninja did affect how the views were implemented, but that’s mostly it. So quite a few benefits without much overhead.

Conclusion

We set out to create a better API interface with the hope that it would minimize API issues and though this project was challenging it was ultimately very interesting and successful. In the end, we guaranteed equality of the API for both backend and frontend and our application was better for it. We will definitely consider it for our next API-heavy project and recommend that if you are starting a new project to check out a similar approach.

Types and API were not the only things we changed though, we also changed how Django integrates React components but if we put that here then this article would be a book so we have a follow-up coming out soon that will look at that. Spoiler, it involves turning React Components into Web Components and it’s pretty awesome, so if you’d be interested in that or if you found this article useful you can follow us on Linkedin, Twitter, and YouTube or bookmark our site, or better yet all of the above.

djangsters GmbH

Vogelsanger Straße 187
50825 Köln