2 First Party Django Rest Api
2 First Party Django Rest Api
Blango Repo
In the Terminal
Clone the repo. Your command should look something like this:
What we will develop will be very primitive, you’ll be able to list all Post
objects, get the full detail for a single Post object, create a new Post object,
update an existing Post object, and finally delete a Post object. So while it
will handle all our use cases, it won’t do much in the way of validation,
error handling or even authentication.
Building this simple API will help cement the REST API concepts without
having to learn all about Django Rest Framework. It will also give us an API
to work with when using Postman, a program for testing HTTP APIs.
We’ll also use the standard HttpResponse class, which will allow us to
return empty responses with status codes to nominate what they mean. For
example, the status code 204 means No Content, that is, the body of the
response is expected to be empty. This indicates that the request was
accepted but doesn’t return any data. To make the code easier to read,
rather than using numeric literals we’ll use the HTTPStatus enum from the
http module (a built in Python library). We return a “204” response like
this:
return HttpResponse(status=HTTPStatus.NO_CONTENT)
In the case of our API, we’ll return this when a Post is updated (PUT
request) or deleted (DELETE request).
The other special status code we’ll use is 201 which means Created. We’ll
return this when a Post is created (POST request to posts list view). The
response will also include the HTTP header Location which contains the
URL of the newly created Post, so that the end user knows how to retrieve
it. We’ll return one like this:
return HttpResponse(
status=HTTPStatus.CREATED,
headers={"Location": reverse("api_post_detail", args=
(post.pk,))},
)
CSRF
CSRF stands for Cross Site Request Forgery, and is an attack that could
occur with a malicious website adding a form to trigger undesired actions
on a target website. To refresh yourself on how Django prevents this with
the CSRF token, check the Cross Site Request Forgery protection
documentation.
Django will reject POST, PUT and DELETE requests that don’t include a CSRF
token, unless the view is marked as not requiring one. This is done with the
django.views.decorators.csrf.csrf_exempt decorator.
For example:
@csrf_exempt
def post_list(request):
…
When you see post_to_dict() used in the view code, it will refer to this
implementation above.
Views
For a GET, it fetches all Post objects, transforms them to dictionaries with
post_to_dict and then returns a JsonResponse:
posts = Post.objects.all()
posts_as_dict = [post_to_dict(p) for p in posts]
return JsonResponse({"data": posts_as_dict})
For a POST request, a new Post is created using values from the request
body (JSON encoded dictionary).
post_data = json.loads(request.body)
post = Post.objects.create(**post_data)
return HttpResponse(
status=HTTPStatus.CREATED,
headers={"Location": reverse("api_post_detail", args=
(post.pk,))},
)
For a PUT, we’ll set the Post object’s attributes using each of the values that
were passed in the request body.
post_data = json.loads(request.body)
for field, value in post_data.items():
setattr(post, field, value)
post.save()
return HttpResponse(status=HTTPStatus.NO_CONTENT)
We’ll return a 204 No Content response, which indicates that the change
was successful. The client will already have the Post objects URL and so can
choose to request the updated data again if it needs to; we don’t need to
pass the Location header.
Finally, for a DELETE request, we can call the Post object’s delete() method,
and return another 204 No Content to indicate success.
post.delete()
return HttpResponse(status=HTTPStatus.NO_CONTENT)
Now, you’ll see the view code in its entirety and you can implement it in
your version of Blango.
Try It Out
Try It Out
Before building our API code, let’s do a tiny bit of housekeeping. The slug
field on Post should be unique. Since we’ve been querying by this field in
our post_detail view then it should probably have been unique from the
start of the project. It hasn’t really been an issue though since we’re just
been creating Post objects through the Django admin as quite a manual
process. Going ahead though, when we start creating them through the API,
we might want to repeat requests as we’re debugging, and don’t want a
bunch of Post objects with the same slug. So the first thing we’ll do is add
the unique=True argument to the SlugField on the Post model, like this:
slug = models.SlugField(unique=True)
We’ll create the views in a file called api_views.py inside the blog
application, to keep them separate from our normal views. Create this file
now.
Open api_views.py
import json
from http import HTTPStatus
@csrf_exempt
def post_list(request):
if request.method == "GET":
posts = Post.objects.all()
posts_as_dict = [post_to_dict(p) for p in posts]
return JsonResponse({"data": posts_as_dict})
elif request.method == "POST":
post_data = json.loads(request.body)
post = Post.objects.create(**post_data)
return HttpResponse(
status=HTTPStatus.CREATED,
headers={"Location": reverse("api_post_detail",
args=(post.pk,))},
)
@csrf_exempt
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.method == "GET":
return JsonResponse(post_to_dict(post))
elif request.method == "PUT":
post_data = json.loads(request.body)
for field, value in post_data.items():
setattr(post, field, value)
post.save()
return HttpResponse(status=HTTPStatus.NO_CONTENT)
elif request.method == "DELETE":
post.delete()
return HttpResponse(status=HTTPStatus.NO_CONTENT)
return HttpResponseNotAllowed(["GET", "PUT", "DELETE"])
We’ve explained the behavior of most of this already, all that’s new is how
we’re branching the code based on the HTTP method from request.method.
Next we’ll set up the URL routes. To keep our main urls.py file from getting
too large, we’ll create a new routing file also in the blog application; call it
api_urls.py. Add this add as the content:
Open api_urls.py
urlpatterns = [
path("posts/", post_list, name="api_post_list"),
path("posts/<int:pk>/", post_detail,
name="api_post_detail"),
]
Finally we’ll add route API requests to this file. Open the main urls.py and
add this to urlpatterns:
Open urls.py
path("api/v1/", include("blog.api_urls")),
Versioning
Note that we’re versioning the URL (by adding the v1 path component). This
will allow us to implement changes to the API without breaking backwards-
compatibility with older clients. While we won’t be using in, Django Rest
Framework has support for versioning which allows you to reuse the same
view for different versions and alter the view’s response based on a special
version attribute that’s available. Read the DRF versioning guide if you do
end up having multiple versions of your real API.
Now start the Django development server if it’s not already running, and
navigate to the Post list API view, the path is /api/v1/posts/. You should
see a list of Post objects in JSON format. The other request you can easily
test in a browser is a Post detail GET. Try viewing something like
/api/v1/posts/1/. You should see a single Post object in JSON format.
View Blog
That’s about the limit of what we can easily test with a web browser. To
make requests other than GET we will use the tool Postman. In the next
section we’ll download and set up Postman and use it to test our basic API.
Pushing to GitHub
Pushing to GitHub
Before continuing, you must push your work to GitHub. In the terminal:
git add .
git commit -m "Finish First-Party Django REST API"
Push to GitHub:
git push