Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Hint: You can adjust the default video playback speed in your account settings.
Hint: You can set your subtitle preferences in your account settings.
Sorry! Looks like there’s an issue with video playback 🙁 This might be due to a temporary outage or because of a configuration issue with your browser. Please refer to our video player troubleshooting guide for assistance.

CRUD Operations

Create, Read, Update, and Delete are the operations you need on all data. As REST is a way of reading and manipulating data on a server, it has HTTP operations for each of these actions. In this lesson, you’ll walk through the complete life cycle of a database object through the Ninja REST API.

00:00 In the previous lesson, I showed you how Ninja serializes data using the Schema and ModelSchema classes. In this lesson, I’m going put it all together and show you a complete data object life cycle.

00:14 CRUD just might be my favorite computer acronym, maybe a close second behind GIMP. CRUD stands for Create, Read, Update, and Delete. Those are the operations you typically need to do with data.

00:29 In REST, that translates to HTTP operations: POST for Create, GET for Read, PUT or PATCH for Update, and DELETE for, well, deleting. One out of four ain’t bad. Ninja provides a decorator for each one of these operations.

00:48 You’ve seen some of it so far, but I’m going to combine it together in this lesson. Do you feel a chill in the air? Add a new Django app, this time called stark.

00:58 Don’t forget to add it to INSTALLED_APPS, register your Ninja routes, and because I’m going to add a model here as well, you’re going to need to run migrations.

01:08 The data I’ll be operating on is the WeddingGift ORM class. I’m keeping it to a single field for demo purposes. You already know how to serialize these.

01:18 Anything more complicated is just a process of adding fields to Schema or letting ModelSchema’s magic do that for you.

01:27 Inside of the stark app, I’ve created schemas.py and inside of it, I’ve defined two ModelSchema classes—one for creating gifts, the other for including them in a response.

01:38 Like I showed in the previous lesson, the ModelSchema is based on your ORM, so I import the WeddingGift class from stark models.

01:47 Then I use the inner-class declaration to connect this ModelSchema to the WeddingGift class. In the previous examples, I listed the fields using the model_fields attribute.

01:58 There’s another attribute you can use instead. That’s model_exclude. In this case, all the fields will be used except for the ID. Of course, that’s just the description field in this case, but you get the idea. For a larger class, this can save you some typing.

02:15 This is a similar declaration for the GiftOut serializer, and this time, I’m using model_fields, but instead of using a list, I’m using the special "__all__" string.

02:26 This says to use all of the fields. The documentation warns against using this. You want to be careful with it. If you’re explicit about your field listing and the ORM class changes, you won’t accidentally expose anything.

02:40 If you use "__all__"—or even model_exclude has the same problem—you are implicitly getting any changes to the ORM without thinking about it.

02:54 With the serializers in place, let’s look at the four CRUD operations. The first is CREATE, using a .post(). In the decorator, in addition to specifying the path, I’m also specifying the response.

03:08 I’m using a GiftOut serializer. I’ve also added the url_name attribute, which is equivalent to the name attribute in a path.

03:18 This registers the call for reverse lookup. This can be a bit finicky, but I’ll come back to that in a second. The view takes its standard request, and because it’s a POST, you need to indicate what is in the request’s body.

03:33 I’m using GiftIn as my payload. Inside the view, you use the ORM’s .create() method, passing in all the fields in the payload.

03:43 The .dict() method on GiftIn returns a dictionary with all the fields sent in. The double-star operator (**) here turns the dictionary into keyword arguments for the create call.

03:55 With the gift ORM created, I return that in the view, the decorator takes care of translating this newly created object using the GiftOut serializer, and the result to the user is JSON.

04:09 Okay, that’s the C in CRUD. Now on to the R. There are actually two Rs—one for listing objects, the other for getting a specific object. Let’s start with the list.

04:20 You’ve seen this before. The .get() decorator takes the path, declares that it will respond with a list of GiftOut serializers, and I’ve added the url_name here as well. Because the response is set to a list of GiftOut serializers, all you need to do is return the .all() call of the WeddingGift ORM. Let me just scroll down a bit.

04:45 Time for our second R. The declaration is similar. It’s still a .get() decorator. This time, the path indicates that it takes an integer named gift_id.

04:56 The response is a single GiftOut serializer, and once again, I’ve named the call using url_name. You may remember that in the targaryen app, I did a GET on the ORM.

05:08 That’s bad practice. By using the .get_object_or_404() shortcut, Ninja will handle both the retrieving of the object and the proper re-raising of the exception if no such object exists. In a later lesson, I’ll show you how to customize Ninja’s exception management. On to the U in CRUD.

05:29 The .put() decorator is used to update an existing object. This means the path needs an ID, similar to the previous .get(). This also responds with a GiftOut serializer. Note the lack of a url_name argument.

05:43 The path here is identical to the .get() path. It is the operation that is different. If you named this one, only one lookup would be kept because Django does the lookup based on the path. So, if called this one update_gift, it would override the .get(), and you’d end up with update_gift as the name for both. It’s best to just name the first usage something generic and not name the others.

06:10 Remember, reverse lookup is based on the path, not on the operation. The view declaration includes both the gift_id and a payload, sort of a mix between a GET and a POST. Here, you’re updating a specific object, so you need the ID of the object in order to fetch it and the fields that you’re going to change. Like with the GET, I look the object up using .get_object_or_404().

06:37 Then, to replace the fields, I look through all of the fields sent in and call .set_attr on each of them. This replaces their current contents with whatever was sent in in the payload. With the fields written to, the object needs to be saved and then just returned. Finally, the DELETE operation is used for the D in CRUD.

07:01 This time, you need a path with an ID, but you don’t declare a response in the decorator. Like the GET, you use the shortcut to fetch the object from the database, then call the ORM’s .delete() method.

07:15 Sometimes you’ll find APIs returning the object that was deleted. This feels fishy to me, as you’re responding with the thing you just removed. I usually just respond with some sort of success indicator.

07:29 Okay, let’s try these out.

07:48 Wow, that was a lot. The -X flag tells curl to use a specific HTTP operation, in this case the POST. The -H flag adds an HTTP header.

08:00 In this case, I’m setting the Content-Type to json because that’s what I’m sending. The Ninja POST decorator expects a single data item in the request body, and that’s supposed to contain a JSON dictionary with fields.

08:16 curl’s -d sets this, "I'm giving the lucky couple a knife." The response is a newly created WeddingGift object, serialized as JSON, {"id": 1, "description": "knife"}.

08:29 Let’s do that again.

08:42 Same idea, but now they’ve got two presents. Let’s look at what has been created by calling the list version of the read.

09:02 No need to use the -X this time, because it’s a GET. And you can see a list comes back with the "knife" and the "stain remover". Let’s update gift number two.

09:32 There were a lot of stains at the Red Wedding. "stain remover" really deserves to be plural. Let’s fetch this item that I’ve just updated.

09:51 And you can see the result. "stain removers" is now plural.

10:04 And finally—well, there’s an appropriate word for the Red Wedding, finally. And finally, I’ve deleted the "stain remover" from the database.

10:13 It probably got all used up. To demonstrate that it worked, I’ll list the gifts one last time.

10:26 And you’re left with just the knife. I-I-I-I want the knife. Now there’s a deep-cut reference that probably only my wife will get. I will give kudos to anybody who in the comments correctly identifies what I’m talking about.

10:45 Let me just quickly show you those url_name arguments in action. I’ve run manage shell and imported the reverse function.

10:59 By default, the NinjaAPI object creates a namespace called api-1.0.0. You can change this if you like. I’ll show you how later. Our first CRUD operation was named create_gift, so to get it, you reverse with the namespace and the name, giving this resulting path: /api/stark/gift.

11:25 And there’s the list reference …

11:33 and the GET, PUT, and DELETE, which use different operations but the same path, can be found under the name "gift". This is actually one of my complaints about Django.

11:44 There should either be a flag for reverse or a different method for doing lookups without the arguments. Frequently, when doing single-page applications, you want pass in the URLs and then append the IDs afterwards as the user does something in the interface.

11:58 I’ve got lots of code and templates that use the URL tag with a fake argument and then JavaScript to strip the argument, just to append a real one later. One of these years, I’ll submit a PR to Django core. But laziness.

12:15 One of the things that comes with Ninja is a web interface for interacting with your API. It gets created automatically at the /docs endpoint of your path.

12:25 Let’s take a look at it.

12:35 Here are all the views that have been created so far. If I click this down arrow on the lannister item, you get a bit of info and a Try it out button.

12:46 There’s not much to see with this call, so it’s kind of empty. Well, let’s push the button. This is a two-step process. You’ll see why in a moment. So now I’m going to push Execute.

13:01 It’s run the call. It even shows you what curl command you could use to get the results on the command line. Let’s do something a little more complicated and create a wedding gift …

13:14 Conveniently color-coded to help you highlight the different kinds of operations.

13:23 When I clicked Try it out, it gave me an editable field pre-populated with a request body. I edit the string, then click Execute. And the third gift was created. Very handy tool, this, especially when you’re debugging your views.

13:48 Next up, I’ll show you a collection of odds and ends, how to write a single view for multiple operations, how to create multiple Ninjas, and using API versioning.

Kojo Idrissa on April 17, 2023

“I, I, I, I want the knife”, followed by “Pleeeease…” is said by Eddy Murphy in The Golden Child when he’s trying to get the sacred dagger. 👴🏾

sgarcia on June 27, 2023

Hi! I’ve had problems executing curl insert and update commands in windows. It looks like it was because of the single quotes (found it thanks to this question: stackoverflow.com/questions/54272167/curl-3-unmatched-brace-in-url-position-1)

So this…:

curl -s -X POST http://127.0.0.1:8000/api/stark/gift -H "Content-Type: application/json" -d '{ "description":"stain remover" }'

…gives me this error:

{“detail”: “Cannot parse request body (Expecting value: line 1 column 1 (char 0))”}

But this works:

curl -s -X POST http://127.0.0.1:8000/api/stark/gift -H "Content-Type: application/json" -d "{ \"description\":\"stain remover\" }"

Hope it can helps someone :)

Become a Member to join the conversation.