How to Build a Scoped Note
If you've built a Django API and you're wondering how to add authentication so that each user can only access their own data, you're in the right place. Most Django tutorials teach you session-based authentication. That works fine when your frontend and backend live on the same server. But the moment you separate them – say, a React app on Netlify talking to a Django API on PythonAnywhere – then sessions start to break down. Cookies don't travel well across different domains, and suddenly your login system stops working. That's where JSON Web Tokens (JWT) come in. JWTs give you a stateless, -free way to authenticate users. They work seamlessly across domains, devices, and platforms. The server doesn't need to remember anything. It just verifies the token's signature and knows exactly who's making the request. But authentication is only half the problem. Once you know who a user is, you still need to control what they can see. This is where scopingcomes in. Scoping means ensuring that each user can only access their own data. User A should never be able to read, edit, or delete User B's data (notes in our case), even if they somehow guess the right ID. In this tutorial, you'll build a a personal note-taking API where users can register, log in with JWT tokens, and store notes that only they can access. Along the way, you'll implement a custom user model, configure SimpleJWT for token-based authentication, and write scoped views that lock each user's data behind their own credentials. Prerequisities What is JWT and Why Use It Over Session Authentication? How Session Authentication Works How JWT Authentication Works Step 1: How to Set Up the Project and Install the Dependecies 1.1 How to Create the Project 1.2 How to Create a Virtual Environment and Install the Required Dependencies 1.3 How to Create the Project and the App 1.4 How to Register the App and Django Rest Framework (DRF) Step 2: How to Create a Custom User Model 2.1 How to Define the Custom User Model 2.2 How to Tell Django to Use Your Custom User Model 2.3 How to Run Migrations Step 3: How to Define the Note Model 3.2 How to Apply Migration 3.3 How to Register Models in the Admin Step 4: How to Create the Serializer 4.1 How to Create UserSerializer 4.2 How to Create NoteSerializer Step 5: How to Configure SimpleJWT 5.1 How to Update REST Framework Settings 5.2 How to Add Token URL Endpoints Step 6: How to Build the Authentication Logic Step 7: How to Implement Scoped Views 7.1 How to Create a NoteViewSet 7.2 Why This Matters: Preventing ID Enumeration Attacks Step 8: How to Connect a URL 8.1 How to Create App-level URLs 8.2 How to Verify the Project-Level URLs Step 9: How to Test the APIs with Postman 9.1 How to Register a User 9.2 How to Obtain Access and Refresh Tokens 9.3 How to Create a Note 9.4 How to List Your Notes 9.5 How to Demostrate Scoping Step 10: How to Handle Token Expiration with Refresh Tokens How You Can Improve This Project Conclusion Here's what this tutorial covers: How to set up a custom user model (and why you should always do this) How to configure SimpleJWT for access and refresh token authentication How to build serializers that protect sensitive fields How to scope your API views so users only see their own data How to test the entire flow using Postman Let's get started Before you begin, make sure you're comfortable with the following: Django fundamentals: You should understand how Django projects and apps work, including models, views, URLs, and migrations. Django REST Framework basics: You should be familiar with serializers, viewsets or API views, and how DRF handles requests and responses. Basic command line usage: You'll run commands in your terminal throughout this tutorial. Tools you'll need installed: Python 3.8 or higher pip (Python's package manager) A code editor like Visual Studio Code Postman (or any API testing tool) for testing your endpoints. You'll use this to send requests to your API. Before you write any code, it's important to understand what problem JWTs solve and why Django's built-in session authentication isn't always enough. Django ships with a session-based authentication system. Here's how it works at a high level: A user sends their username and password to the server. The server verifies the credentials and creates a sessionwhich is a small record stored in the server's database that says "this user is logged in." The server sends back a session IDas a . The browser stores this automatically. On every subsequent request, the browser sends the back to the server. The server looks up the session ID in its database and says "ah, this is User A. Let them through." This works perfectly when your frontend and backend live on the same domain. The browser handles s automatically, and Django manages sessions in the database without you thinking about it. But this approach has some limitations. The cross-domain problem:If your React frontend lives at app.example.com and your Django API lives at api.example.com, s become tricky. Browsers enforce strict rules about which domains can send and receive s. You can work around this with CORS (Cross-Origin Resource Sharing) headers and special settings, but it adds complexity and can be fragile. The scalability problem:Every active session is stored in the server's database. If you have 10,000 users logged in at the same time, that's 10,000 session records the server has to look up on every single request. As your application grows, this lookup becomes a bottleneck. The mobile problem:Mobile apps don't handle s the same way browsers do. If you're building an API that will serve both a web app and a mobile app, session s create extra headaches. JWTs take a fundamentally different approach. Instead of storing session data on the server, they put the authentication information directly into the token itself. Here's how the flow works: A user sends their username and password to the server. The server verifies the credentials and creates a JWT – a long encoded string that contains information like the user's ID and when the token expires. The server sends this token back to the client. The client stores it (usually in memory or local storage). On every subsequent request, the client includes the token in the request header. The server reads the token, verifies its signature, and says "this is User A. Let them through." Notice the key difference: the server never stores anything. It doesn't look up a session in a database. It simply reads the token, checks its cryptographic signature to make sure nobody tampered with it, and extracts the user information. That's why JWTs are called stateless– the server doesn't maintain any state about who is logged in. This solves the cross-domain problembecause tokens are sent in the request header, not as s. Headers work the same way regardless of which domain the request comes from. This solves the scalability problembecause the server doesn't store sessions. Verifying a token is a quick cryptographic check, not a database lookup. This solves the mobile problembecause any client that can send HTTP headers can use JWT. Mobile apps, desktop apps, other servers – they all work the same way. Open your terminal, navigate to where you want your project to live, and run the following commands: You will create a virtual environment here. Type the following command: The above command creates a virtual environment inside a folder called To activate the virtual environment, we need to use the following command: On macOS/Linux: On Windows: You'll know it worked when you see With the virutal environment activated, install Django, Django Rest Framework, and Simple JWT Framework using the command: You can verify everything installed correctly by running: You should see all three packages listed along with their dependencies. Run the following command to create the Django project: The dot at the end is important. It tells Django to create the project files in your current directory instead of creating an extra nested folder. Now let's type this command to create the app: Open Django now knows about your new app and the REST framework. Let's move on to the most important architectural decision you'll make for this project. If you've built Django projects before, you might have used Django's default User model. For quick prototypes, that works fine. But for any project you plan to grow or maintain, starting with a custom user model is a best practice you should never skip. Here's why: Django's default Using a custom user model gives you full control over what a "user" means in your app. Instead of being tied to a username, you can design login around something more practical, like email or phone_number for a fitness or mobile-based app. You can also include fields like role (doctor, patient, receptionist in a clinic system) or date of birth directly in the user model, instead of managing a separate profile. It also helps future-proof your project. If you start with the default model and later decide to switch login from username to email, or add required fields, it becomes difficult and risky to change. Using a custom user model from the beginning avoids this problem and makes it much easier to adapt your authentication system as your app grows. By creating a custom user model from the start, even if it's identical to the default one, you give yourself the freedom to make changes later without any of that pain. Open You are importing Django’s built-in Think of The But the key point is that this model is yours. So this model behaves exactly like Django’s default user model, but with one big advantage: you now have the flexibility to customize it later. If three months from now you need to add a You can also see all the fields that the To do this we can use the Python shell. Type the following command: When you type this command, make sure that the virtual environment is active: After this, import the After that, type the following code: The above statement lists out all the fields in the Now comes the important bit. Open This setting tells Django to use your There's no strict rule to where you need to add it, but the best practice is to add it near the end of the file. You can see which user model Django is using by using the method Open the Python shell again and import the Then use You should see the name of our model being used: If you hadn't added the Note:You'll need to do this before you run your first migration. If you run migrate before setting AUTH_USER_MODEL, Django creates tables for the default User model, and switching afterward becomes a headache. Now create and apply the initial migrations: Django will create the necessary tables for your custom user model along with all the built-in Django tables. We can again peek under the hood to see the SQL queries that Django used to create the tables especially the Type this command: Here And you should get this output: Let's also create a superuser so you can access the admin panel later for debugging: Fill in the username, email (optional), and password when prompted. Now let's create the data model for the core of your application. First add a new import to use the Then add the following code below the Here's the complete Let's walk through each field: Notice that we use The The The Run the migration commands to create the Note table: As before, we can see the exact SQL query Django used to create the Open This is helpful during development when you want to quickly check whether data is being saved correctly. In DRF, a serializer is like a bridge between your database and the internet. Django models store data as Python objects. But when you want to send that data to a frontend application (like React or a mobile app), you can't send Python objects. You need to send a format that everyone understands which is usually JSON. Serializers perform three main jobs: Serialization:Converting complex Python objects (Models) into Python dictionaries (which can be easily rendered into JSON). Deserialization:Converting JSON data coming from a user back into complex Python objects. Validation:Checking if the incoming data is correct before saving it to the database. Create a new file called Let's break down this serializer. The A When we use a 1. Generates fields from the model so you don't have to So users can create accounts, but their passwords are never exposed back. The It's important to understand why we have overridden this method. The default By default this method stores the password in plain text format. This is a serious problem because passwords should never be stored in raw form. They need to be hashedso that even if the database is compromised, the passwords are never exposed. Django provides a special method called After the First of all, you need to add an import to the Put this code below the Now let's break it down: Without this protection, a malicious user could send a POST request with The Here is the complete code in the Now let's set up the authentication system. This is where you tell DRF to use JWT for authentication instead of sessions. This step is crucial because without it, DRF will default to session-based auth. SimpleJWT provides a complete JWT implementation for DRF, so you don't have to build token generation, signing, or verification from scratch. The access token is what your client sends with every API request. It's short-lived by design. Think of it like a visitor badge at an office building: it gets you through the door, but it expires at the end of the day. If someone steals it, the damage is limited because it stops working soon. The refresh token is longer-lived and has a single purpose: getting a new access token when the current one expires. The client stores it securely and only sends it to one specific endpoint. Think of it like your employee ID card. You use it to get a new visitor badge each morning, but you don't flash it at every door. This separation exists for security. If the short-lived access token is compromised (which is more likely since it's sent with every request), the attacker has a narrow window before it expires. The refresh token, which is sent less frequently, has a lower risk of interception. Let's look at how the access and refresh token work together User logs in, server gives both access token and refresh token User makes requests using the access token Access token expires App sends refresh token to server Server checks it and gives a new access token User continues without logging in again Open Let's unpack what each section does. The The This is a secure-by-default approach: instead of remembering to protect each view individually, everything is protected, and you explicitly open up the endpoints that need to be public (like the registration endpoint, which you'll handle in the next step). The When the access token expires, the client can use the refresh token to get a new access token without forcing the user to log in again. The duration of the refresh token is 1 day. This means after 1 day, the user must log in again with their username and password. You'll see exactly how this works later when you test with Postman. SimpleJWT provides ready-made views for obtaining and refreshing tokens. You just need to wire them up to URLs. Open The The Open Now let's walk through this code. The first section are the imports and after that we have used the the Now the main part is Because of this, you don’t have to manually write the logic for handling POST requests, validating data, or saving to the database. DRF does all of that for you behind the scenes. Inside the class, The Finally This means that anyone can access the registration endpoint, even if they aren't logged in. This makes sense for a registration endpoint because new users won’t have accounts yet. Every other view in your API will inherit the global IsAuthenticated permission, so only this registration endpoint is open. This is the heart of the tutorial. You've set up authentication so the API knows whois making a request. Now you need to make sure each user can only interact with theirownnotes. Think of it this way: authentication is the lock on the front door of an apartment building. It keeps strangers out. But scoping is the lock on each individual apartment. Just because you live in the building doesn't mean you can walk into your neighbor's apartment. Without scoping, an authenticated user could potentially see every note in the database, or worse, modify notes that belong to someone else. Two method overrides on your viewset prevent this entirely. Now let's create the Add the following to Now let's talk about this code in detail. You've created a new class called The next part But the magic is the two methods that you are overriding: The But here, you've overridden this method so that it filters notes by the current user. Next is the Notice that you have passed Remember how you made the owner field read-only in the serializer? This is the other half of that security measure. The user can't set the owner through the API request, and the server automatically sets it to whoever is authenticated. These two pieces work together to make ownership tamper-proof. Without get_queryset filtering, your API might allow something like this: a user sends a GET request to This is called an ID enumeration attack— an attacker cycles through IDs (1, 2, 3, 4...) to discover and access other people's data. With your scoped Now you need to wire up the views to URL paths so the API knows which view to call for each endpoint. Create a new file called The The Make sure your Here's the full picture of your API's URL structure: Start the development server to make sure everything runs without errors: If the server starts without complaints, your code is wired up correctly. Building the API is one thing. Proving it works is another. Let's walk through the entire flow using Postman, from registering a user to demonstrating that scoping actually works. If you haven't used Postman before, it's a tool that lets you send HTTP requests to your API and inspect the responses. You can download it from postman.com/downloads. Alternatively, you can use curl from the command line or any other API testing tool you're comfortable with. Make sure your development server is running before proceeding. Open Postman: Create a new request: Click Send. You should get a Now log in to get your JWTs: You'll get a response with access and refresh tokens. Copy the access token.You'll need it for every subsequent request. Also save the refresh token, as you'll use it later. A JWT is only encoded and not encrypted. The encoding is merely a way to transform the data into a safe, standard string format that can be easily transmitted over the internet. Any one can peel through the encoding to see the data. This is done using base64url encoding. We can use the Python library For this demo, we'll use site called jwt.io. Open the site and paste in the access token that you have just created: The JWT has three parts: the header, the payload, and the signature. The header sections tells you how the header is signed. In this case it is signed using the HS256algorithm. The payload is where the actual data or claim lives. It contains standard claims such as token types, expiration time ( The signature section is used to verify integrity. You can't decode it to meaningful data.This section ensures that the token wasn't tampered with. Now use the access token to create a note: Notice that you don't include an owner field. That's handled automatically by perform_create. You should get a You can create a few more notes, so that we have some data to work with. Now to fetch all of Priya's notes: You should see all the notes created, sorted by most recent first. Let's prove that a second user can't view the first user's notes. First, register the second user. Send a POST request to Then get tokens for Sujan by sending a POST request to Now send a GET request to The response should be an empty list since this user hasn't created any notes: More importantly, Priya's notes are completely invisible to him. Even if Sujan tries to access a specific note by ID – say, This is intentional. A A Now that you know why we've used the First, I'll access Priya's individual note using her credentials and her access token: Now, I'll change the access token and put Sujan's (new user) access token: You can see that using the new user's token to access the previous user's note leads to Access tokens are deliberately short-lived (30 minutes in your configuration). This limits the window of damage if a token is stolen. But you don't want users to re-enter their credentials every 30 minutes. That's what refresh tokens are for. When Priya's access token expires, her API requests will start returning Replace your old access token with this new one, and you're good for another 30 minutes. The refresh token itself lasts for one day, so the user only needs to fully log in again once every 24 hours. In a real application, the frontend client handles this automatically. When an API call returns a Here's what that flow looks like in pseudocode: Client sends request with access token Server responds with 401 (token expired) Client sends refresh token to /api/token/refresh/ Server responds with a new access token Client retries the original request with the new access token Server responds with the data If the refresh token itself has expired (after 24 hours in your configuration), step 4 will also return a This API is functional and secure, but there's plenty of room to build on it. Here are some directions you could take. Add search and filtering.Let users search their notes by title or body text. You can use DRF's SearchFilter and django-filter to add query parameters like Add categories or tags.Create a Add pagination.Once a user has hundreds of notes, returning them all in a single response becomes slow. DRF has built-in pagination classes that let you return notes in pages of 10, 20, or whatever size you choose. Deploy to a production server.The API currently runs on your local machine. You could deploy it to platforms like PythonAnywhere, Railway, or Render to make it accessible from anywhere. You'd need to configure a production database (like PostgreSQL), set a secure SECRET_KEY, and serve the application behind HTTPS. Build a frontend.Connect a React, Next.js, or Vue.js frontend to this API. Store the JWTs in the client and implement the token refresh flow so users stay logged in seamlessly. Add token blacklisting.SimpleJWT supports token blacklisting, which lets you invalidate refresh tokens when a user logs out. Without this, a refresh token remains valid until it expires, even after the user "logs out." Each of these improvements builds on the patterns you've already learned and will deepen your understanding of Django, DRF, and API design. You've built a fully functional, secure note-taking API with Django, Django REST Framework, and SimpleJWT. Along the way, you learned some fundamental concepts that apply to any API you'll build in the future. You started with a custom user model — a small decision at the beginning that saves you from a painful migration later. You configured JWT authentication so your API can serve mobile clients and decoupled frontends that can't rely on session s. You built serializers that protect sensitive data by keeping passwords write-only and ownership read-only. Most importantly, you implemented scoped views that ensure each user's data is completely isolated from everyone else's. The patterns you practiced here — overriding The best way to solidify what you've learned is to keep building. Try adding search and filtering, build a React frontend that consumes this API, or start a completely new project may be a task manager, a journal app, or a bookmarks API using the same JWT and scoping patterns. The core workflow stays the same. Only the models and business logic change.What We'll Cover:
Prerequisities
What is JWT and Why Use It Over Session Authentication?
How Session Authentication Work

How JWT Authentication Works

Step 1: How to Set Up the Project and Install the Dependecies
1.1 How to Create the Project
mkdir notes-projectcd notes-project
1.2 How to Create a Virtual Environment and Install the Required Dependencies
python3 -m venv venv
venv. The first venvis the command and the second venvrepresents the name of the folder. You can name the folder anything though venvis usually preferred.source venv/bin/activatevenv\Scripts\activate(venv)at the beginning of your terminal prompt. From this point on, any Python packages you install will only exist inside this virtual environment.
pip install django djangorestframework djangorestframework-simplejwt 
pip list
1.3 How to Create the Project and the App
django-admin startproject notes_core .python manage.py startapp notes
1.4 How to Register the App and Django Rest Framework (DRF)
notes_core/settings.pyand add rest_frameworkand notesin the INSTALLED_APPSlist:
Step 2: How to Create a Custom User Model
Usermodel uses a usernamefield as the primary identifier. If you later decide you want users to log in with their email address instead, or you need to add a profile picture field, or a phone number, then you're stuck.2.1 How to Define the Custom User Model
notes/models/pyand add the following code:from django.contrib.auth.models import AbstractUserfrom django.db import modelsclass CustomUser(AbstractUser): pass
AbstractUserclass.AbstractUseras a ready-made blueprint for a user. It already includes fields like username, password, email, first name, last name , and authentication logic.passstatement means you're not adding any extra fields yet.phone_numberfield or switch to email-based login, you just add a field to this class and run a migration.from django.contrib.auth.models import AbstractUserfrom django.db import modelsclass CustomUser(AbstractUser): phone_number = models.CharField(max_length=15)CustomUserclass has inherited from the AbstractUserclass.python manage.py shell
CustomUsermodel in the shell:from notes.models import CustomUser[fields.name for field in CustomUser._meta.get_fields()]CustomUserclass.
2.2 How to Tell Django to Use Your Custom User Model
notes_core/settings.pyand add this line:AUTH_USER_MODEL = 'notes.CustomUser'CustomUsermodel instead of the built-in one for everything authentication-related such as login, permissions, foreign keys, and so on.
get_user_model().get_user_model()method:from django.contrib.auth import get_user_model get_user_model()and print the output:user = get_user_model()print(user)
AUTH_USER_MODELin the settings.pyfile, then Django would have used the default user model:
2.3 How to Run Migrations
python manage.py makemigrationspython manage.py migrate
CustomUsertable.python manage.py sqlmigrate notes 0001notesis the name of the app and 0001represents the migration number.
python manage.py createsuperuser
Step 3: How to Define the Note Model
settingsobject.from django.conf import settingsCustomUserclass:class Notes(models.Model): owner = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='notes' ) title = models.CharField(max_length=200) body = models.TextField() created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{ self.title} (by { self.owner.username})"model.pycode:from django.contrib.auth.models import AbstractUserfrom django.db import modelsfrom django.conf import settingsclass CustomUser(AbstractUser): passclass Notes(models.Model): owner = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='notes' ) title = models.CharField(max_length=200) body = models.TextField() created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{ self.title} (by { self.owner.username})"
owner = models.ForeignKey(settings.AUTH_USER_MODEL, ...): Creates a relationship between each note and a user. The ForeignKeyfield tells Django that each note belogs to exactly one user but a user can have many notes.settings.AUTH_USER_MODELinstead of directly importing CustomUser. This is the recommended practice because it keeps your code flexible. If you ever change the user model reference in settings, this foreign key adapts automatically.on_delete=models.CASCADEmeans that if a user is deleted, all their notes are deleted too.related_name='notes'lets you access a user's notes with user.notes.all().title = models.CharField(max_length=200): Creates a text field for the task name, limited to 200 characters.body = models.TextField(): Holds the actual note content. TextFieldhas no character limit, so users can write as much as they need.created_at = models.DateTimeField(auto_now_add=True): Automatically records the date and time when a task is created. You never need to set this manually.__str__()method gives each note a readable representation. Instead of seeing "Note object (1)" in the admin panel or during debugging, you'll see something like "Meeting Notes (by Solina)."3.2 How to Apply Migration
python manage.py makemigrationspython manage.py migrate
notestable:
3.3 How to Register Models in the Admin
notes/admin.pyand register both models so you can inspect data through the admin panel:from django.contrib import adminfrom .models import CustomUser, Notesadmin.site.register(CustomUser)admin.site.register(Notes)
Step 4: How to Create the Serializer

4.1 How to Create UserSerializer
notes/serializers.pyand add the following code:from rest_framework import serializersfrom django.contrib.auth import get_user_modelUser = get_user_model()class UserSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True) class Meta: model = User fields = ['id', 'username', 'email', 'password'] def create(self, validated_data): user = User.objects.create_user( username=validated_data['username'], email=validated_data.get('email', ''), password=validated_data['password'] ) return userUserSerializerhandles user registration.User = get_user_model()gets the user model that you're using and stores in the variable User. In our case, we're using the CustomUsermodelclass UserSerializer(serializers.ModelSerializer):: Here you've created the UserSerializer class, which inherits ModelSerializer.ModelSerializeris a shortcut that automatically creates a serializers class with fields that are in the model class.ModelSerializer, DRF inspects the model and automatically does these things:
2. Automatically adds field validations that are present in the model
3. Implements create()and update()methods. A ModelSerializerknows which model to use and how to update and create it. You can override create()and update()methods if you need customized behaviors. You have overridden thecreate()method in the above code.password = serializers.CharField(write_only=True): This line is crucial. The write_only=Trueflag means the password will be accepted during registration but will neverappear in any API response. Without this, your API would send back the password (even if hashed) every time user data is returned.class Meta: Inside the Metaclass, you tell the serializer which model to use. In this case, the model to use is Userand the fields to be handled.create()method: This is the most important part. This method runs when we create a new user. Instead of using the default .create()method you have overridden it.create()method is not suitable for creating users securely.create_user()that automatically handles this by hashing the passwordand setting up the user properly for authentication.
4.2 How to Create NoteSerializer
UserSerializerclass, let's create the NoteSerializerclass. The NoteSerializerhandles the notes dataNotesclass. Add the line from .models import Notesat the end of the last import.UserSerializerclass:class NoteSerializer(serializers.ModelSerializer): owner = serializers.ReadOnlyField(source='owner.username') class Meta: model = Notes fields = ['id', 'owner', 'title', 'body', 'created_at']owner = serializers.ReadOnlyField(source='owner.username'): This is the most important line in the code. This makes the ownerfield read-only. That means the API will display who owns a note (showing their username), but no one can set or change the owner through the API."owner": 5and assign their note to someone else's account, or worse, modify someone else's notes by reassigning ownership.source='owner.username'part tells DRF to display the owner's username instead of their numeric ID, which makes the API responses more readable.class Meta:...: As before the Metaclass contains the model which the serializer use and the fields that the API will expose.serializers.pyfilefrom rest_framework import serializersfrom django.contrib.auth import get_user_modelfrom .models import NotesUser = get_user_model()class UserSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True) class Meta: model = User fields = ['id', 'username', 'email', 'password'] def create(self, validated_data): user = User.objects.create_user( username=validated_data['username'], email=validated_data.get('email', ''), password=validated_data['password'] ) return userclass NoteSerializer(serializers.ModelSerializer): owner = serializers.ReadOnlyField(source='owner.username') class Meta: model = Notes fields = ['id', 'owner', 'title', 'body', 'created_at']
Step 5: How to Configure SimpleJWT

5.1 How to Update REST Framework Settings
notes_core/settings.pyand add the following code:from datetime import timedeltaREST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ),}SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), 'REFRESH_TOKEN_LIFETIME': timedelta(days=1),}
DEFAULT_AUTHENTICATION_CLASSESsetting tells DRF to use JWT as the authentication method for all API endpoints. Every incoming request will be checked for a valid JWT token in the Authorization header.DEFAULT_PERMISSION_CLASSESsetting sets IsAuthenticatedas the global permission policy. This means every endpoint in your API is locked down by default. Only users with a valid token can access any endpoint.SIMPLE_JWTdictionary controls token behavior. The access token lasts 30 minutes. This is the token clients include in every request. If someone intercepts it, the damage is limited to a 30-minute window. The refresh token lasts one day.5.2 How to Add Token URL Endpoints
notes_core/urls.pyand update it with the following code:from django.contrib import adminfrom django.urls import path, includefrom rest_framework_simplejwt.views import ( TokenObtainPairView, TokenRefreshView,)urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('notes.urls')), path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),]token/endpoint accepts a username and password, and returns an access token and a refresh token.token/refresh/endpoint accepts a refresh token and returns a new access token. You'll see these in action during testing.Step 6: How to Build the Authentication Logic
notes/views.pyand add the following:from rest_framework import generics, permissionsfrom django.contrib.auth import get_user_modelfrom .serializers import UserSerializerUser = get_user_model()class RegisterView(generics.CreateAPIView): queryset = User.objects.all() serializer_class = UserSerializer permission_classes = [permissions.AllowAny]get_user_model()method to get the CustomUsermodel.RegisterViewclass. The class inherits from generics.CreateAPIViewwhich is a built in DRF view designed specifically for handling POST requests that create new objects.queryset = Users.objects.all()defines the set of user objects this view can work with.serializer_class = UserSerializertells the view which serializer to use for validating incoming data and creating the user.permission_classes = [permissions.AllowAny]overrides the global IsAuthenticatedpermission you set earlier in the value of DEFAULT_PERMISSION_CLASSES.Step 7: How to Implement Scoped Views

7.1 How to Create a NoteViewSet
NoteViewSet. First add these imports to the top of the file. We're importing the viewsets, serializers, and model.from .models import Notefrom .serializers import UserSerializer, NoteSerializerfrom rest_framework import generics, viewsets, permissionsnotes/views.py, below the RegisterView:class NoteViewSet(viewsets.ModelViewSet): serializer_class = NoteSerializer def get_queryset(self): return Notes.objects.filter(owner=self.request.user).order_by('-created_at') def perform_create(self, serializer): serializer.save(owner=self.request.user)NoteViewSetwhich inherits from the DRF class ModelViewSet. This gives you full CRUD operations, meaning you can list notes and retrieve a single note, as well as create, update, and delete a note.
serializer_class = NoteSerializertells Django to use the NoteSerializerclass to convert between Python objects and JSON.get_queryset()and perform_create().get_queryset()method controls which notes the API returns. If you didn't override this method, it would return Note.objects.all()(which would give every user access to every note in the database).perform_create()method, which is called when the note is saved. You've overridden this method so that it saves the notes of the user who's currently logged in. If you hadn't overridden the this method, it would return all the notes regardless of the logged in user.self.request.userparameters in to the filter()function. This is the code that attaches the logged-in user as the owner of the note.
7.2 Why This Matters: Preventing ID Enumeration Attacks
/api/notes/42/and sees a note that belongs to someone else, simply because they guessed the ID.get_queryset, even if User B sends a request to /api/notes/42/and note 42 belongs to User A, the viewset won't find it in User B's filtered queryset. DRF will return a 404 — as far as User B is concerned, that note doesn't exist.Step 8: How to Connect a URL
8.1 How to Create App-level URLs
notes/urls.pyand add the following:from django.urls import path, includefrom rest_framework.routers import DefaultRouterfrom .views import RegisterView, NoteViewSetrouter = DefaultRouter()router.register(r'notes', NoteViewSet, basename='note')urlpatterns = [ path('register/', RegisterView.as_view(), name='register'), path('', include(router.urls)),]DefaultRouterautomatically generates URL patterns for the NoteViewSet. Since you're using a ModelViewSet, the router creates endpoints for listing all notes, creating a note, retrieving a single note, updating a note, and deleting a note — all from that single router.register call.basename='note'parameter is required here because your viewset doesn't have a queryset attribute defined directly on the class (you're using get_queryset instead). DRF uses the basenameto generate the URL pattern names like note-listand note-detail.8.2 How to Verify the Project-Level URLs
notes_core/urls.pylooks like this (you set this up in Step 5, but let's confirm):from django.contrib import adminfrom django.urls import path, includefrom rest_framework_simplejwt.views import ( TokenObtainPairView, TokenRefreshView,)urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('notes.urls')), path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),]Endpoint Method Description api/register/POST Create a new user account api/token/POST Get access and refresh tokens api/token/refresh/POST Get a new access token using a refresh token api/notes/GET List all notes for the authenticated user api/notes/POST Create a new note api/notes/<id>/GET Retrieve a specific note api/notes/<id>/PUT/PATCH Update a specific note api/notes/<id>/DELETE Delete a specific note python manage.py runserverStep 9: How to Test the APIs with Postman


9.1 How to Register a User

Method POST URL http://127.0.0.1:8000/api/register/Body tab Select "raw" and choose "JSON" from the dropdown Body Content { "username": "priya", "email": "[email protected]", "password": "securepassword123" } 
201 Createdresponse with the user data (without the password, thanks to your write_only=Truefield) which you wrote in the UserSerializerclass.

9.2 How to Obtain Access and Refresh Tokens
Method POST URL http://127.0.0.1:8000/api/token/Body { "username" : "priya", "password" : "securepassword123"} 
pyjwtto decode JWTs or use any of the online sites to decode. It's important to note that you should use online sites with caution since JWTs may contain sensitive information.
exp), issued at time ( iat), and custom claims.9.3 How to Create a Note
Method POST URL http://127.0.0.1:8000/api/notes/Header tab: Add a new header: Key: Authorization, Value: Bearer Body { 'title': 'My note', 'body': 'This contains secret information'} 
201 Created response:
9.4 How to List Your Notes
Method GET URL http://127.0.0.1:8000/api/notes/Header tab: Same Authorization: Bearer header 
9.5 How to Demonstrate Scoping
http://127.0.0.1/api/registerwith the following data:Method POST URL http://127.0.0.1:8000/api/register/Body tab Select "raw" and choose "JSON" from the dropdown Body Content { "username": "sujan", "email": "[email protected]", "password": "anotherpassword123" } 
http://127.0.0.1:8000/api/token/with Sujan's credentials (username and password) and then copy Sujan's access token.
http://127.0.0.1:8000/api/notes/using Sujan's token in the Authorization header.
http://127.0.0.1:8000/api/notes/1/– he'll get a 404 Not Foundresponse, not a 403 Forbidden.404 Not Founddoesn't reveal that the note exists, while a 403 Forbiddenwould confirm its existence to a potential attacker.403 Forbiddenresponse is like a door with a sign: “Authorized personnel only”.You now know something important is inside. A 404 Not Foundresponse is like a blank wall. You don’t even know a room exists.
404response instead of 403, let's demonstrate this.

404 Not Foundresponse.Step 10: How to Handle Token Expiration with Refresh Tokens

401 Unauthorizedresponses. Instead of logging in again, the client sends the refresh token to get a fresh access token.Method POST URL http://127.0.0.1:8000/api/token/refresh/Body tab Select "raw" and choose "JSON" from the dropdown Body Content { refresh: < Priya's refresh token >} 
401, the client catches it, sends the refresh token to get a new access token, and retries the original request — all without the user noticing.
401. At that point, the user truly needs to log in again with their username and password. This is the intended behavior: it means even a stolen refresh token has a limited useful life.How You Can Improve This Project
?search=meetingto the notes list endpoint.Categorymodel and add a foreign keyto Note, or use a many-to-many relationship for tags. This would let users organize their notes and filter by category.Conclusion
get_querysetto filter by the current user, overriding perform_createto assign ownership automatically, and using read-onlyfields to prevent data tampering — are the same patterns you'll use in production APIs handling real user data.
-
上一篇
-
下一篇
- 最近发表
- 随机阅读
-
- Testimonial Collection System
- Financial Planning Platform
- Virtual Summit Organization
- Observability Platform Setup
- How to Choose the Best Stock Market API for FinTech Projects and AI Agents
- Accounts Payable Automation
- Event-Driven Architecture
- Survey Tools for Market Research
- rotateY()
- Backward Compatibility Guide
- System Integration Strategy
- Error Handling Best Practices
- Bhavin Sheth
- Login Security Best Practices
- Predictive Maintenance Platform
- Tissue Bank System
- How to Build a Browser
- Success Story Documentation
- Sorting Algorithm Examples
- Gradient Generator Tools Tutorial
- 搜索
-