The simplest API

Published:

So, I saw a post recently about Build an API under 30 lines of code using Flask.

I started wondering what it would take to do the same in Django.

The two main tools we're going go use are JsonResponse and ModelForm.

JsonResponse is a sub-class of HttpResponse that JSON encodes the data you pass it.

And the ModelForm has a way to pull field values from a model instance.

So... let's create a project:

$ virtualenv -p python3 env
$ . env/bin/activate
$ pip install Django
$ django-admin startproject salary
$ cd salary

Add our project as an app, by adding "salary", to INSTALLED_APPS.

Models

Into salary/models.py put:

from django.db import models


class Salary(models.Model):
    name = models.CharField(max_length=200)
    position_title = models.CharField(max_length=200)
    department = models.CharField(max_length=200, db_index=True)
    salary = models.DecimalField(max_digits=10, decimal_places=2)

And create the migration:

$ python manage.py makemigrations salary
$ python manage.py migrate

Forms

Into salary/forms.py put:

from django import forms

from .models import Salary


class SalaryForm(forms.ModelForm):
    class Meta:
        model = Salary
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if 'instance' in kwargs:
            for field in self.fields.values():
                field.required = False

Views

Next, we need to start providing some views.

So let's create salary/views.py:

from django.http import JsonResponse
from django.views.generic import ListView, DetailView

from .forms import SalaryForm
from .models import Salary


class JsonMixin:
    response_class = JsonResponse
    content_type = 'application/json'
    form_class = SalaryForm

    def reduce_object(self, obj):
        form = self.form_class(instance=obj)
        return form.initial

    def render_to_response(self, context, **kwargs):
        return self.response_class(context, safe=False, **kwargs)

    def render_errors(self, form, **kwargs):
        data = {
            key: list(value)
            for key, value in form.errors.items()
        }
        return self.response_class(data, status=400)


class SalaryList(JsonMixin, ListView):
    model = Salary

    def get_context_data(self, **kwargs):
        return [
            self.reduce_object(obj)
            for obj in self.get_queryset()
        ]

    def post(self, request, *args, **kwargs):
        form = self.form_class(request.POST)
        if form.is_valid():
            obj = form.save()
            return self.render_to_response(self.reduce_object(obj))
        return self.render_errors(form)


class SalaryDetail(JsonMixin, DetailView):
    model = Salary
    response_class = JsonResponse

    def get_context_data(self, **kwargs):
        return self.reduce_object(self.get_object())

    def post(self, request, *args, **kwargs):
        form = self.form_class(request.POST, instance=self.get_object())
        if form.is_valid():
            obj = form.save()
            return self.render_to_response(self.reduce_object(obj))
        else:
            return self.render_errors(form)

URLs

And finally, update our salary/urls.py:

from django.conf.urls import url
from django.views.decorators.csrf import csrf_exempt

from . import views


urlpatterns = [
    url(r'^$', csrf_exempt(views.SalaryList.as_view())),
    url(r'(?P<pk>\d+)/$', csrf_exempt(views.SalaryDetail.as_view())),
]

Helper

Now an import script to bring the CSV records.

We create import.py which is just a simple loop using csv.DictReader and our ModelForm.

import sys

from csv import DictReader

import django

django.setup()

from salary.models import Salary
from salary.forms import SalaryForm


# Name,Position Title,Department,Employee Annual Salary
NAME = {
    'Name': 'name',
    'Position Title': 'position_title',
    'Department': 'department',
    'Employee Annual Salary': 'salary',
}

for row in DictReader(open(sys.argv[1])):
    rec = {
        NAME[key]:  value.strip().strip('$')
        for key, value in row.items()
    }
    form = SalaryForm(rec)
    if form.is_valid():
        form.save()
    else:
        print(form.errors)

Now import the data:

$ python import.py Current_Employee_Names__Salaries__and_Position_Titles.csv

Result

$ wc -l salary/forms.py salary/views.py salary/urls.py salary/models.py
  15 salary/forms.py
  58 salary/views.py
  10 salary/urls.py
   8 salary/models.py
  91 total

So, it's a few more than 30 lines, but it also uses only django -- no 3rd party apps.

A GET to '/' returns a list looking like:

[
  {
    "salary": "90744.00",
    "department": "WATER MGMNT",
    "name": "AARON,  ELVIA J",
    "position_title": "WATER RATE TAKER",
    "id": 1
  },
  {
    "salary": "84450.00",
    "department": "POLICE",
    "name": "AARON,  JEFFERY M",
    "position_title": "POLICE OFFICER",
    "id": 2
  },

... and so on.

You can POST to that URL to create a new record, with helpful error messages:

{
  "salary": [
    "This field is required."
  ],
  "department": [
    "This field is required."
  ],
  "position_title": [
    "This field is required."
  ]
}

A GET to /(pk)/ will yield a single record:

{
  "salary": "90744.00",
  "department": "WATER MGMNT",
  "name": "Walker McNulty",
  "position_title": "WATER RATE TAKER",
  "id": 1
}

And POSTing to that same URL can update the record. The __init__ method in the form ensures all fields are optional.

I would have preferred to use PUT to update, but it's actually not so easy to get Django to parse a POST body on a PUT request because, technically, it can't assume one.