Writing API views
Now, we will create Django views that will use the previously created GameSerializer
class to return JSON representations for each HTTP request that our API will handle. Open the games/views.py
file. The following lines show the initial code for this file, with just one import statement and a comment that indicates we should create the views.
from django.shortcuts import render # Create your views here.
The following lines show the new code that creates a JSONResponse
class and declares two functions: game_list
and game_detail
, in the games/views.py
file. We are creating our first version of the API, and we use functions to keep the code as simple as possible. We will work with classes and more complex code in the next examples. The highlighted lines show the expressions that evaluate the value of the request.method
attribute to determine the actions to be performed based on the HTTP verb. The code file for the sample is included in the restful_python_chapter_01_01
folder:
from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from rest_framework.renderers import JSONRenderer from rest_framework.parsers import JSONParser from rest_framework import status from games.models import Game from games.serializers import GameSerializer class JSONResponse(HttpResponse): def __init__(self, data, **kwargs): content = JSONRenderer().render(data) kwargs['content_type'] = 'application/json' super(JSONResponse, self).__init__(content, **kwargs) @csrf_exempt def game_list(request): if request.method == 'GET': games = Game.objects.all() games_serializer = GameSerializer(games, many=True) return JSONResponse(games_serializer.data) elif request.method == 'POST': game_data = JSONParser().parse(request) game_serializer = GameSerializer(data=game_data) if game_serializer.is_valid(): game_serializer.save() return JSONResponse(game_serializer.data, status=status.HTTP_201_CREATED) return JSONResponse(game_serializer.errors, status=status.HTTP_400_BAD_REQUEST) @csrf_exempt def game_detail(request, pk): try: game = Game.objects.get(pk=pk) except Game.DoesNotExist: return HttpResponse(status=status.HTTP_404_NOT_FOUND) if request.method == 'GET': game_serializer = GameSerializer(game) return JSONResponse(game_serializer.data) elif request.method == 'PUT': game_data = JSONParser().parse(request) game_serializer = GameSerializer(game, data=game_data) if game_serializer.is_valid(): game_serializer.save() return JSONResponse(game_serializer.data) return JSONResponse(game_serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == 'DELETE': game.delete() return HttpResponse(status=status.HTTP_204_NO_CONTENT)
The JSONResponse
class is a subclass of the django.http.HttpResponse
class. The superclass represents an HTTP response with a string as content. The JSONResponse
class renders its content into JSON. The class defines just declare the __init__
method that created a rest_framework.renderers.JSONRenderer
instance and calls its render
method to render the received data into JSON save the returned bytestring in the content
local variable. Then, the code adds the 'content_type'
key to the response header with 'application/json'
as its value. Finally, the code calls the initializer for the base class with the JSON bytestring
and the key-value pair added to the header. This way, the class represents a JSON response that we use in the two functions to easily return a JSON response.
The code uses the @csrf_exempt
decorator in the two functions to ensure that the view sets a Cross-Site Request Forgery (CSRF) cookie. We do this to make it simple to test this example that doesn't represent a production-ready Web Service. We will add security features to our RESTful API later.
When the Django server receives an HTTP request, Django creates an HttpRequest
instance, specifically a django.http.HttpRequest
object. This instance contains metadata about the request, including the HTTP verb. The method
attribute provides a string representing the HTTP verb or method used in the request.
When Django loads the appropriate view that will process the requests, it passes the HttpRequest
instance as the first argument to the view function. The view function has to return an HttpResponse
instance, specifically a django.http.HttpResponse
instance.
The game_list
function lists all the games or creates a new game. The function receives an HttpRequest
instance in the request
argument. The function is capable of processing two HTTP verbs: GET
and POST
. The code checks the value of the request.method
attribute to determine the code to be executed based on the HTTP verb. If the HTTP verb is GET
, the expression request.method == 'GET'
will evaluate to True
and the code has to list all the games. The code will retrieve all the Game
objects from the database, use the GameSerializer
to serialize all of them, and return a JSONResponse
instance built with the data generated by the GameSerializer
. The code creates the GameSerializer
instance with the many=True
argument to specify that multiple instances have to be serialized and not just one. Under the hoods, Django uses a ListSerializer
when the many
argument value is set to True
.
If the HTTP verb is POST
, the code has to create a new game based on the JSON data that is included in the HTTP request. First, the code uses a JSONParser
instance and calls its parse method with request as an argument to parse the game data provided as JSON
data in the request and saves the results in the game_data
local variable. Then, the code creates a GameSerializer
instance with the previously retrieved data and calls the is_valid
method to determine whether the Game
instance is valid or not. If the instance is valid, the code calls the save
method to persist the instance in the database and returns a JSONResponse with the saved data in its body and a status equal to status.HTTP_201_CREATED
, that is, 201 Created
.
Tip
Whenever we have to return a specific status different from the default 200 OK
status, it is a good practice to use the module variables defined in the rest_framework.status
module and to avoid using hardcoded numeric values.
The game_detail
function retrieves, updates or deletes an existing game. The function receives an HttpRequest
instance in the request
argument and the primary key or identifier for the game to be retrieved, updated or deleted in the pk
argument. The function is capable of processing three HTTP verbs: GET
, PUT
and DELETE
. The code checks the value of the request.method
attribute to determine the code to be executed based on the HTTP verb. No matter which is the HTTP verb, the function calls the Game.objects.get
method with the received pk
as the pk
argument to retrieve a Game instance from the database based on the specified primary key or identifier, and saves it in the game
local variable. In case a game with the specified primary key or identifier doesn't exist in the database, the code returns an HttpResponse
with its status equal to status.HTTP_404_NOT_FOUND
, that is, 404 Not Found
.
If the HTTP verb is GET
, the code creates a GameSerializer
instance with game
as an argument and returns the data for the serialized game in a JSONResponse
that will include the default 200 OK
status. The code returns the retrieved game serialized as JSON.
If the HTTP verb is PUT
, the code has to create a new game based on the JSON data that is included in the HTTP request and use it to replace an existing game. First, the code uses a JSONParser
instance and calls its parse method with request as an argument to parse the game data provided as JSON
data in the request and saves the results in the game_data
local variable. Then, the code creates a GameSerializer
instance with the Game
instance previously retrieved from the database (game
) and the retrieved data that will replace the existing data (game_data
). Then, the code calls the is_valid
method to determine whether the Game
instance is valid or not. If the instance is valid, the code calls the save
method to persist the instance with the replaced values in the database and returns a JSONResponse
with the saved data in its body and the default 200 OK
status. If the parsed data doesn't generate a valid Game instance, the code returns a JSONResponse
with a status equal to status.HTTP_400_BAD_REQUEST
, that is, 400 Bad Request
.
If the HTTP verb is DELETE
, the code calls the delete
method for the Game
instance previously retrieved from the database (game
). The call to the delete
method erases the underlying row in the games_game
table, and therefore, the game won't be available anymore. Then, the code returns a JSONResponse
with a status equal to status.HTTP_204_NO_CONTENT
that is, 204 No Content
.
Now, we have to create a new Python file named urls.py
in the games
folder, specifically, the games/urls.py
file. The following lines show the code for this file that defines the URL patterns that specifies the regular expressions that have to be matched in the request to run a specific function defines in the views.py
file. The code file for the sample is included in the restful_python_chapter_01_01
folder:
from django.conf.urls import url from games import views urlpatterns = [ url(r'^games/$', views.game_list), url(r'^games/(?P<pk>[0-9]+)/$', views.game_detail), ]
The urlpatterns
list makes it possible to route URLs to views. The code calls the django.conf.urls.url
function with the regular expression that has to be matched and the view function defined in the views module as arguments to create a RegexURLPattern
instance for each entry in the urlpatterns
list.
We have to replace the code in the urls.py
file in the gamesapi
folder, specifically, the gamesapi/urls.py
file. The file defines the root URL configurations, and therefore, we must include the URL patterns declared in the previously coded games/urls.py
file. The following lines show the new code for the gamesapi/urls.py
file. The code file for the sample is included in the restful_python_chapter_01_01
folder:
from django.conf.urls import url, include urlpatterns = [ url(r'^', include('games.urls')), ]
Now, we can launch Django's development server to compose and send HTTP requests to our unsecure Web API (we will definitely add security later). Execute the following command:
python manage.py runserver
The following lines show the output after we execute the preceding command. The development server is listening at port 8000
.
Performing system checks... System check identified no issues (0 silenced). May 20, 2016 - 04:22:38 Django version 1.9.6, using settings 'gamesapi.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C.
With the preceding command, we will start Django development server and we will only be able to access it in our development computer. The preceding command starts the development server in the default IP address, that is, 127.0.0.1
(localhost
). It is not possible to access this IP address from other computers or devices connected on our LAN. Thus, if we want to make HTTP requests to our API from other computers or devices connected to our LAN, we should use the development computer IP address, 0.0.0.0
(for IPv4 configurations), or ::
(for IPv6 configurations) as the desired IP address for our development server.
If we specify 0.0.0.0
as the desired IP address for IPv4 configurations, the development server will listen on every interface on port 8000. When we specify ::
for IPv6 configurations, it will have the same effect. In addition, it is necessary to open the default port 8000
in our firewalls (software and/or hardware) and configure port-forwarding to the computer that is running the development server. The following command launches Django's development server in an IPv4 configuration and allows requests to be made from other computers and devices connected to our LAN:
python manage.py runserver 0.0.0.0:8000
Tip
If you decide to compose and send HTTP requests from other computers or devices connected to the LAN, remember that you have to use the development computer's assigned IP address instead of localhost
. For example, if the computer's assigned IPv4 IP address is 192.168.1.106
, instead of localhost:8000
, you should use 192.168.1.106:8000
. Of course, you can also use the host name instead of the IP address. The previously explained configurations are very important because mobile devices might be the consumers of our RESTful APIs and we will always want to test the apps that make use of our APIs in our development environments.