This post walks through the process of developing a CRUD-based RESTful API with Django and Django REST Framework, which is used for rapidly building RESTful APIs based on Django models.
NOTE: Check out the third Real Python course for a more in-depth tutorial on Django REST Framework.
This application uses:
Python v3.6.0
Django v1.11.0
Django REST Framework v3.6.2
Postgres v9.6.1
Psycopg2 v2.7.1
Objectives
By the end of this tutorial you will be able to…
Discuss the benefits of using Django REST Framework for bootstrapping the development of a RESTful API
Validate model querysets using serializers
Appreciate Django REST Framework’s Browsable API feature for a cleaner and well-documented version of your APIs
Practice test-driven development
Why Django REST Framework?
Django REST Framework (REST Framework) provides a number of powerful features out-of-the-box that go well with idiomatic Django, including:
Browsable API: Documents your API with a human-friendly HTML output, providing a beautiful form-like interface for submitting data to resources and fetching from them using the standard HTTP methods.
Auth Support: REST Framework has rich support for various authentication protocols along with permissions and throttling policies which can be configured on a per-view basis.
Serializers: Serializers are an elegant way of validating model querysets/instances and converting them to native Python datatypes that can be easily rendered into JSON and XML.
Throttling: Throttling is way to determine whether a request is authorized or not and can be integrated with different permissions. It is generally used for rate limiting API requests from a single user.
Plus, the documentation is easy to read and full of examples. If you’re building a RESTful API where you have a one-to-one relationship between your API endpoints and your models, then REST Framework is the way to go.
Next, define global settings for REST Framework in a single dictionary, again, in the settings.py file:
1234567
# REST FrameworkREST_FRAMEWORK={# Use Django's standard `django.contrib.auth` permissions,# or allow read-only access for unauthenticated users.'DEFAULT_PERMISSION_CLASSES':[],'TEST_REQUEST_DEFAULT_FORMAT':'json'}
This allows unrestricted access to the API and sets the default test format to JSON for all requests.
NOTE: Unrestricted access is fine for local development, but in a production environment you may need to restrict access to certain endpoints. Make sure to update this. Review the docs for more information.
Your current project structure should now look like:
Next, define a puppy model with some basic attributes in django-puppy-store/puppy_store/puppies/models.py:
1234567891011121314151617181920
fromdjango.dbimportmodelsclassPuppy(models.Model):""" Puppy Model Defines the attributes of a puppy """name=models.CharField(max_length=255)age=models.IntegerField()breed=models.CharField(max_length=255)color=models.CharField(max_length=255)created_at=models.DateTimeField(auto_now_add=True)updated_at=models.DateTimeField(auto_now=True)defget_breed(self):returnself.name+' belongs to '+self.breed+' breed.'def__repr__(self):returnself.name+' is added.'
Hop into psql again and verify that the puppies_puppy has been created:
12345678910111213141516171819
$ psql
# c puppy_store_drfYou are now connected to database "puppy_store_drf".
puppy_store_drf=# dt List of relations
Schema | Name | Type | Owner
--------+----------------------------+-------+----------------
public | auth_group | table | michael.herman
public | auth_group_permissions | table | michael.herman
public | auth_permission | table | michael.herman
public | auth_user | table | michael.herman
public | auth_user_groups | table | michael.herman
public | auth_user_user_permissions | table | michael.herman
public | django_admin_log | table | michael.herman
public | django_content_type | table | michael.herman
public | django_migrations | table | michael.herman
public | django_session | table | michael.herman
public | puppies_puppy | table | michael.herman
(11 rows)
NOTE: You can run d+ puppies_puppy if you want to look at the actual table details.
Before moving on, let’s write a quick unit test for the Puppy model.
Add the following code to a new file called test_models.py in a new folder called “tests” within “django-puppy-store/puppy_store/puppies”:
1234567891011121314151617181920
fromdjango.testimportTestCasefrom..modelsimportPuppyclassPuppyTest(TestCase):""" Test module for Puppy model """defsetUp(self):Puppy.objects.create(name='Casper',age=3,breed='Bull Dog',color='Black')Puppy.objects.create(name='Muffin',age=1,breed='Gradane',color='Brown')deftest_puppy_breed(self):puppy_casper=Puppy.objects.get(name='Casper')puppy_muffin=Puppy.objects.get(name='Muffin')self.assertEqual(puppy_casper.get_breed(),"Casper belongs to Bull Dog breed.")self.assertEqual(puppy_muffin.get_breed(),"Muffin belongs to Gradane breed.")
In the above test, we added dummy entries into our puppy table via the setUp() method from django.test.TestCase and asserted that the get_breed() method returned the correct string.
Add an __init__.py file to “tests” and remove the tests.py file from “django-puppy-store/puppy_store/puppies”.
Let’s run our first test:
12345678
(env)$ python manage.py testCreating test database for alias'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.007s
OK
Destroying test database for alias'default'...
Great! Our first unit test has passed!
Serializers
Before moving on to creating the actual API, let’s define a serializer for our Puppy model which validates the model querysets and produces Pythonic data types to work with.
Add the following snippet to django-puppy-store/puppy_store/puppies/serializers.py:
In the above snippet we defined a ModelSerializer for our puppy model, validating all the mentioned fields. In short, if you have a one-to-one relationship between your API endpoints and your models – which you probably should if you’re creating a RESTful API – then you can use a ModelSerializer to create a Serializer.
With our database in place, we can now start building the RESTful API…
RESTful Structure
In a RESTful API, endpoints (URLs) define the structure of the API and how end users access data from our application using the HTTP methods – GET, POST, PUT, DELETE. Endpoints should be logically organized around collections and elements, both of which are resources.
In our case, we have one single resource, puppies, so we will use the following URLS – /puppies/ and /puppies/<id> for collections and elements, respectively:
Routes and Testing (TDD)
We will be taking a test-first approach rather than a thorough test-driven approach, wherein we will be going through the following process:
add a unit test, just enough code to fail
then update the code to make it pass the test.
Once the test passes, start over with the same process for the new test.
Begin by creating a new file, django-puppy-store/puppy_store/puppies/tests/test_views.py, to hold all the tests for our views and create a new test client for our app:
12345678910
importjsonfromrest_frameworkimportstatusfromdjango.testimportTestCase,Clientfromdjango.urlsimportreversefrom..modelsimportPuppyfrom..serializersimportPuppySerializer# initialize the APIClient appclient=Client()
Before starting with all the API routes, let’s first create a skeleton of all view functions that return empty responses and map them with their appropriate URLs within the django-puppy-store/puppy_store/puppies/views.py file:
fromrest_framework.decoratorsimportapi_viewfromrest_framework.responseimportResponsefromrest_frameworkimportstatusfrom.modelsimportPuppyfrom.serializersimportPuppySerializer@api_view(['GET','DELETE','PUT'])defget_delete_update_puppy(request,pk):try:puppy=Puppy.objects.get(pk=pk)exceptPuppy.DoesNotExist:returnResponse(status=status.HTTP_404_NOT_FOUND)# get details of a single puppyifrequest.method=='GET':returnResponse({})# delete a single puppyelifrequest.method=='DELETE':returnResponse({})# update details of a single puppyelifrequest.method=='PUT':returnResponse({})@api_view(['GET','POST'])defget_post_puppies(request):# get all puppiesifrequest.method=='GET':returnResponse({})# insert a new record for a puppyelifrequest.method=='POST':returnResponse({})
Create the respective URLs to match the views in django-puppy-store/puppy_store/puppies/urls.py:
With all routes now wired up with the view functions, let’s open up REST Framework’s Browsable API interface
and verify whether all the URLs are working as expected.
First, fire up the development server:
1
(env)$ python manage.py runserver
Make sure to comment out all the attributes in REST_FRAMEWORK section of our settings.py file, to bypass
login. Now visit http://localhost:8000/api/v1/puppies
You will see an interactive HTML layout for the API response. Similarly we can test the other URLs and verify all URLs are working perfectly fine.
Let’s start with our unit tests for each route.
Routes
GET ALL
Start with a test to verify the fetched records:
123456789101112131415161718192021
classGetAllPuppiesTest(TestCase):""" Test module for GET all puppies API """defsetUp(self):Puppy.objects.create(name='Casper',age=3,breed='Bull Dog',color='Black')Puppy.objects.create(name='Muffin',age=1,breed='Gradane',color='Brown')Puppy.objects.create(name='Rambo',age=2,breed='Labrador',color='Black')Puppy.objects.create(name='Ricky',age=6,breed='Labrador',color='Brown')deftest_get_all_puppies(self):# get API responseresponse=client.get(reverse('get_post_puppies'))# get data from dbpuppies=Puppy.objects.all()serializer=PuppySerializer(puppies,many=True)self.assertEqual(response.data,serializer.data)self.assertEqual(response.status_code,status.HTTP_200_OK)
@api_view(['GET','POST'])defget_post_puppies(request):# get all puppiesifrequest.method=='GET':puppies=Puppy.objects.all()serializer=PuppySerializer(puppies,many=True)returnResponse(serializer.data)# insert a new record for a puppyelifrequest.method=='POST':returnResponse({})
Here, we get all the records for puppies and validate each using the PuppySerializer.
Run the tests to ensure they all pass:
123
Ran 2 tests in 0.072s
OK
GET Single
Fetching a single puppy involves two test cases:
Get valid puppy – e.g., the puppy exists
Get invalid puppy – e.g., the puppy does not exists
Add the tests:
12345678910111213141516171819202122232425
classGetSinglePuppyTest(TestCase):""" Test module for GET single puppy API """defsetUp(self):self.casper=Puppy.objects.create(name='Casper',age=3,breed='Bull Dog',color='Black')self.muffin=Puppy.objects.create(name='Muffin',age=1,breed='Gradane',color='Brown')self.rambo=Puppy.objects.create(name='Rambo',age=2,breed='Labrador',color='Black')self.ricky=Puppy.objects.create(name='Ricky',age=6,breed='Labrador',color='Brown')deftest_get_valid_single_puppy(self):response=client.get(reverse('get_delete_update_puppy',kwargs={'pk':self.rambo.pk}))puppy=Puppy.objects.get(pk=self.rambo.pk)serializer=PuppySerializer(puppy)self.assertEqual(response.data,serializer.data)self.assertEqual(response.status_code,status.HTTP_200_OK)deftest_get_invalid_single_puppy(self):response=client.get(reverse('get_delete_update_puppy',kwargs={'pk':30}))self.assertEqual(response.status_code,status.HTTP_404_NOT_FOUND)
Run the tests. You should see the following error:
@api_view(['GET','UPDATE','DELETE'])defget_delete_update_puppy(request,pk):try:puppy=Puppy.objects.get(pk=pk)exceptPuppy.DoesNotExist:returnResponse(status=status.HTTP_404_NOT_FOUND)# get details of a single puppyifrequest.method=='GET':serializer=PuppySerializer(puppy)returnResponse(serializer.data)
In the above snippet, we get the puppy using an ID. Run the tests to ensure they all pass.
POST
Inserting a new record involves two cases as well:
classCreateNewPuppyTest(TestCase):""" Test module for inserting a new puppy """defsetUp(self):self.valid_payload={'name':'Muffin','age':4,'breed':'Pamerion','color':'White'}self.invalid_payload={'name':'','age':4,'breed':'Pamerion','color':'White'}deftest_create_valid_puppy(self):response=client.post(reverse('get_post_puppies'),data=json.dumps(self.valid_payload),content_type='application/json')self.assertEqual(response.status_code,status.HTTP_201_CREATED)deftest_create_invalid_puppy(self):response=client.post(reverse('get_post_puppies'),data=json.dumps(self.invalid_payload),content_type='application/json')self.assertEqual(response.status_code,status.HTTP_400_BAD_REQUEST)
@api_view(['GET','POST'])defget_post_puppies(request):# get all puppiesifrequest.method=='GET':puppies=Puppy.objects.all()serializer=PuppySerializer(puppies,many=True)returnResponse(serializer.data)# insert a new record for a puppyifrequest.method=='POST':data={'name':request.data.get('name'),'age':int(request.data.get('age')),'breed':request.data.get('breed'),'color':request.data.get('color')}serializer=PuppySerializer(data=data)ifserializer.is_valid():serializer.save()returnResponse(serializer.data,status=status.HTTP_201_CREATED)returnResponse(serializer.errors,status=status.HTTP_400_BAD_REQUEST)
Here, we inserted a new record by serializing and validating the request data before inserting to the database.
Run the tests again to ensure they pass.
You can also test this out with the Browsable API. Fire up the development server again, and navigate to http://localhost:8000/api/v1/puppies/. Then, within the POST form, submit the following as application/json:
classUpdateSinglePuppyTest(TestCase):""" Test module for updating an existing puppy record """defsetUp(self):self.casper=Puppy.objects.create(name='Casper',age=3,breed='Bull Dog',color='Black')self.muffin=Puppy.objects.create(name='Muffy',age=1,breed='Gradane',color='Brown')self.valid_payload={'name':'Muffy','age':2,'breed':'Labrador','color':'Black'}self.invalid_payload={'name':'','age':4,'breed':'Pamerion','color':'White'}deftest_valid_update_puppy(self):response=client.put(reverse('get_delete_update_puppy',kwargs={'pk':self.muffin.pk}),data=json.dumps(self.valid_payload),content_type='application/json')self.assertEqual(response.status_code,status.HTTP_204_NO_CONTENT)deftest_invalid_update_puppy(self):response=client.put(reverse('get_delete_update_puppy',kwargs={'pk':self.muffin.pk}),data=json.dumps(self.invalid_payload),content_type='application/json')self.assertEqual(response.status_code,status.HTTP_400_BAD_REQUEST)
@api_view(['GET','DELETE','PUT'])defget_delete_update_puppy(request,pk):try:puppy=Puppy.objects.get(pk=pk)exceptPuppy.DoesNotExist:returnResponse(status=status.HTTP_404_NOT_FOUND)# get details of a single puppyifrequest.method=='GET':serializer=PuppySerializer(puppy)returnResponse(serializer.data)# update details of a single puppyifrequest.method=='PUT':serializer=PuppySerializer(puppy,data=request.data)ifserializer.is_valid():serializer.save()returnResponse(serializer.data,status=status.HTTP_204_NO_CONTENT)returnResponse(serializer.errors,status=status.HTTP_400_BAD_REQUEST)# delete a single puppyelifrequest.method=='DELETE':returnResponse({})
In the above snippet, similar to an insert, we serialize and validate the request data and then respond appropriately.
Run the tests again to ensure that all the tests pass.
DELETE
To delete a single record, an ID is required:
123456789101112131415161718
classDeleteSinglePuppyTest(TestCase):""" Test module for deleting an existing puppy record """defsetUp(self):self.casper=Puppy.objects.create(name='Casper',age=3,breed='Bull Dog',color='Black')self.muffin=Puppy.objects.create(name='Muffy',age=1,breed='Gradane',color='Brown')deftest_valid_delete_puppy(self):response=client.delete(reverse('get_delete_update_puppy',kwargs={'pk':self.muffin.pk}))self.assertEqual(response.status_code,status.HTTP_204_NO_CONTENT)deftest_invalid_delete_puppy(self):response=client.delete(reverse('get_delete_update_puppy',kwargs={'pk':30}))self.assertEqual(response.status_code,status.HTTP_404_NOT_FOUND)
@api_view(['GET','DELETE','PUT'])defget_delete_update_puppy(request,pk):try:puppy=Puppy.objects.get(pk=pk)exceptPuppy.DoesNotExist:returnResponse(status=status.HTTP_404_NOT_FOUND)# get details of a single puppyifrequest.method=='GET':serializer=PuppySerializer(puppy)returnResponse(serializer.data)# update details of a single puppyifrequest.method=='PUT':serializer=PuppySerializer(puppy,data=request.data)ifserializer.is_valid():serializer.save()returnResponse(serializer.data,status=status.HTTP_204_NO_CONTENT)returnResponse(serializer.errors,status=status.HTTP_400_BAD_REQUEST)# delete a single puppyifrequest.method=='DELETE':puppy.delete()returnResponse(status=status.HTTP_204_NO_CONTENT)
Run the tests again. Make sure all of them pass. Make sure to test out the UPDATE and DELETE functionality within the Browsable API as well!
Conclusion and Next Steps
In this tutorial, we went through the process of creating a RESTful API using Django REST Framework with a test-first approach.
What’s next? To make our RESTful API robust and secure, we can implement permissions and throttling for a production environment to allow restricted access on the basis of authentication credentials and rate limiting to avoid any sort of DDoS attack. Also, don’t forget to prevent the Browsable API from being accessible in a production environment.
Feel free to share your comments, questions, or tips in the comments below. The full code can be found in the django-puppy-store repository.