-
Principles:
- Batteries included: project comes together with all possible parts required for full usability
- Convention over configuration: Core components (models, views, serializers, forms, etc) have a predefined structure
-
Built-in features:
- Django comes with ORM, migrations, authentication, admin panel out of the box
-
Explict design:
- Layers have specific roles:
- Models: define data
- Serializers: handle data transformation
- Views: manage request/response logic
- URLS: map views to endpoints
- Layers have specific roles:
- Flexibility
- Configuration over convention
- Architectural decisions rely mainly on the developer
- Apps: Inside the project, there can be multiple apps Project/ ├── App1/ │ ├── models/ │ ├── views/ │ ├── urls/ ├── App2/ │ ├── models/ │ ├── views/ │ ├── urls/
python3 -m venv env # create virtual env
source env/bin/activate # activate virtual env
echo $VIRTUAL_ENV # to check if we're inside a virtual environment
which python # another possibility
# Add requirements.txt to root, then run:
pip install -r requirements.txt
# requirements.txt
asgiref
Django
django-cors-headers
djangorestframework
djangorestframework-simplejwt
PyJWT
pytz
sqlparse
psycopg2-binary
python-dotenv
django-admin startproject server # create the backend
cd server # go to server directory
python manage.py startapp api # start an app
- We need to customize the settings in the project-level slightly
from datetime import timedelta
from dotenv import load_dotenv
import os
load_dotenv()
...
ALLOWED_HOSTS = ["*"]
# JWT Settings
REST_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),
}
INSTALLED_APPS = [
...
'django.contrib.staticfiles',
# add them
"api",
"rest_framework",
"corsheaders"
]
MIDDLEWARE = [
...
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# add it
"corsheaders.middleware.CorsMiddleware"
]
... # at the bottom
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = True
- Once installed, the dependencies will be on env > lib > python_version > list of dependencies
- Json Web Token
- Commonly used in authentication and authorization flows
{
"headers": {
"Authorization": "Bearer XXXX.YYYY.ZZZZ"
// header.payload.signature
}
}
- The backend needs to know who we're and which permissions we have
- Client signs in/registers. User enters login crendentials
- Server verifies credentials and returns JWT
- This JWT contains claims about the user's permission to application/website
- Now, the JWT works as a "badge", which goes with the user where he goes. It's like we're in a factory and through the "badge" the user has some access or no to the structure of the factory
- Never store JWT in local storage or session data
- Instead, they should be stored in a HttpOnly cookie. This isn't accessible from JavaScript/browser code
- Client submits token in every future request 3.1. This is what allows the client to access routes/services/resources
JWTs are composed of:
-
Header => XXXXXX.
- The header is further divided into two pieces, being:
- 1.1 Type of token
- 1.2 Type of algorithm being used
{ "type": "Bearer", "algorithm": "RSA" }
- This information is encoded with "base64urlEncoded"
- The header is further divided into two pieces, being:
-
Payload => YYYYYY.
- Contains the claims: information about the client, entity or any additional data
- 2.1 Registered claims
- 2.2 Public claims
- 2.3 Private claims
// each of these lines is a claim { "username": "Matt", "admin": true, "exp": 12345678, // unix format }
- Only store sensitive data in your payload if it's encrypted
- Encryption: transforming human-readable plain text into incomprehensibe text (ciphertext)
- Digital signature: a type of electronic signature that encrypts docs with digital codes
- Contains the claims: information about the client, entity or any additional data
-
Signature => ZZZZZZ.
- Encoded header + encoded payload + SECRET_KEY
- Takes all this info to check if the data has been tempered with or if it's reliable
- Access token: what will grant us access. What is used with the requests
- Refresh token: used to refresh the access token
- Once the access token is expired, the refresh token will be submitted to the server. If the refresh token is valid, a new access token will be generated
Once the front-end receives them, it will store them in the cookies. So, we don't need to keep revalidating all the time.
They are like interpreters
-
Serializers in Django REST Framework (DRF) are used to:
- Convert our data into JSON format and vice versa
- Convert Python objects (like Django models) into JSON, which can be consumed by the frontend
- Validate incoming JSON data and convert it into Python objects for backend use
-
JSON: JavaScript Object Notation, a lightweight data-interchange format
-
ORM: Object-Relational Mapping, a programming technique for converting data between different systems
- Python ORM: We write Python code, and it transforms it into database instructions
# project > app > serializers.py
# server > api > serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User # the model to serialize
fields = ["id", "username", "password"] # the fields to serialize
extra_kwargs = {"password": {"write_only": True}} # make sure the password is not returned in the response
# we override the create method
def create(self, validated_data):
# User.objects.create_user => handles password hashing
# by using it, we automatically hash the password before storing them
user = User.objects.create_user(**validated_data) # ** is used to unpack the dictionary
return user
- Views in DRF handle HTTP requests and response. They:
- Receive requests (ex: POST to create a user)
- Process data (ex: validate, save, or fetch data)
- Return appropriate responses (ex: JSON for success or error messages)
# project > app > views.py
# server > api > views.py
# generics.CreateAPIView: simplifies the creation of a resource by reducing boilerplat
class CreateUserView(generics.CreateAPIView):
queryset = User.objects.all() # queryset: specify the data this view works with
serializer_class = UserSerializer # serializer_class: specify the serializer used to validate and transform data
permission_classes = [AllowAny] # permission_classes: defines who can access this view
Handle common CRUD operations with minimal boilerplate code. They are3 based on querysets and serializers.
- They provide functionality such as: list, retrieve, create, update, destroy
class BlogPostListView(generics.ListAPIView):
queryset = BlogPost.objects.all()
serializer_class = BlogPostSerializers
Types:
- ListAPIView: GET list all
- CreatAPIView: POST
- RetrievedAPIView: GET single entity
- UpdateAPIView: PUT or PATCH
- DestroyAPIView: DELETE
- ListCreateAPIView: GET (list) + POST
- RetrieveUpdateDestoryAPIView: GET(single entity) + PUT/PATCH + DELETE
Most basic DRF view. It gives full control over the request handling, but requires more boilerplate
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
class BlogPostAPIView(APIView):
def get(self, request):
posts = BlogPost.objects.all()
serializer = BlogPostSerializer(posts, many=True) # doing something similar to zod
return Response(serializer.data)
def post(self, request):
serializer = BlogPostSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Request: Client -> URL -> View -> Serializer -> DB Response: DB -> Serializer -> View -> Client
Client
↓ POST /api/users/
Django URLs (urls.py)
↓ Match URL pattern → Route to view
View (views.py)
↓ Handle request → Call serializer
Serializer (serializer.py)
↓ Validate → Process → Save to DB
Database (models.py)
↓ Save user → Return Python object
View (views.py)
↓ Conver Python object → JSON response
Client
-
Frontend sends a request (ex:
POST /api/users/
) to the backend. Request usually includes:- URL: endpoint for the action (
api/users
) - HTTP Method: defines the action (POST, GET, DELETE, PUT)
- Headers: metadata, like
Content-Type
or authorization tokens - Body: the data payload
- URL: endpoint for the action (
-
Django URLS receive the request and direct to the appropriate view
urlpatterns = [ path('users/', CreateUserView.as_view(), name='create_user'), ] # when the backend servers the POST /api/users/ django matches the URL pattern and calss the CreateUserView.as_view()
-
View receives and handles the request
- Retrieves and validates the incoming data
- Uses the serializer to transform and process ata
- Interacts with the db to perform operation
- Prepares the response
-
Serializer will validate and convert the data
- Validates the incoming data
- Converts JSON into Python for backend use
- Handles logic
- Converts Python objects back to JSON for the response
-
Response is returned to the frontend with the created user data
- Go to:
server/api/users/register
-> create the user - Go to:
server/api/token
-> add credentials and get token- This will give us a refresh and access token. This is what the front-end would store
- In case we want to refresh the access token, go to:
server/api/token/refresh
and add the refresh token there. It will generate a new access token.
Define the model -> Apply Migrations -> Create Serializer -> Create View -> Configure URL -> Test the API Model -> Migration -> Serializer -> View -> URL (MMSVU: My mother sings very uniquely)
- Define the model: Create the database structure for the data
- Create model in app's
model.py
- Define the fields and their types
from django.db import models
class BlogPost(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
- Apply Migrations: Sync the model with the database
python manage.py makemigrations # generate migration files
python manage.py migrate # apply the migrations to the db
- Create the serializer: Define how the model's data is converted to/from JSON
- Create the serializer in app's
serializers.py
- Use
ModelSerializer
for most use cases
from rest_framework import serializers
from .models import BlogPost
class BlogPostSerializer(serializers.ModelSerializer):
class Meta:
model = BlogPost
fields = ['id', 'title', 'content', 'created_at', 'updated_at']
- Create Views: handle requests and perform actions (CRUD)
- Use DRF generic views for common actions (ex: CRUD)
- Assign the serializer and queryset to the view
from rest_framework import generics
from .models import BlogPost
from .serializers import BlogPostSerializer
class BlogPostListCreateView(generics.ListCreateAPIView):
queryset = BlogPost.objecs.all()
serializer_class = BlogPostSerializer
class BlogPostDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = BlogPost.objects.all()
serializer_class = BlogPostSerializer
- Configure the URL: Map the views to specific endpoints
- Add URL pattern in project's
urls.py
from django.urls import path
from .views import BlogPostListCreateView, BlogPostDetailView
urlsPatterns = [
path("blogposts/", BlogPostListCreateView.as_view(), name="blogpost-list-create"),
path("blogposts/<int:pk>/", BlogPostDetailView.as_view(), name="blogpost-detail")
]
- Django has a built-in authentication, which includes login, logout, password management, and permissions
- Configure the Middleware in the project's
settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
]
}
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.admin',
...
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
...
]
- Create the Serializer and the View in the project's app
project > app > serializers.py
# how the user looks like | validation of the user
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User # the model to serialize
fields = ["id", "username", "password"] # the fields to serialize
extra_kwargs = {"password": {"write_only": True}} # make sure the password is not returned in the response
def create(self, validated_data):
user = User.objects.create_user(**validated_data) # ** is used to unpack the dictionary
return user
project > app > views.py
class CreateUserView(generics.CreateAPIView):
queryset = User.objects.all() # queryset: specify the data this view works with
serializer_class = UserSerializer # serializer_class: specify the serializer used to validate and transform data
permission_classes = [AllowAny] # permission_classes: defines who can access this view
- Add the authentication views on the urls path
urlpatterns = [
...
path("api/user/register/", CreateUserView.as_view(), name="register"), # link register view
path("api/user/logout/", LogoutView.as_view(), name="logout"),
# api-auth
path("api/token/", TokenObtainPairView.as_view(), name="get_token"), # link TokenObtainPairView
path("api/token/refresh/", TokenRefreshView.as_view(), name="refresh"), # link TokenRefresh view
path("api-auth/", include("rest_framework.urls")), # link pre-built rest_frameworks
...
]
- Creating Logout logic
project > app > views.py
class LogoutView(APIView):
permission_classes = [AllowAny]
parser_classes = [JSONParser, FormParser] # Enable JSON and form-data parsing
def post(self, request):
try:
print(request.data)
refresh_token = request.data.get("refresh")
print(refresh_token)
if not refresh_token:
return Response({"error": "Refresh token is required"}, status=400)
token = RefreshToken(refresh_token)
if token.get("token_type") != "refresh":
return Response({"error": "Invalid token type. Refresh token required."}, status=400)
token.blacklist() # add the token to the blacklist
return Response({"message": "You have been logged out"}, status=200)
except Exception as e:
return Response({"error": str(e)}, status=400)
- Roles
- Product Owner: Defines the product vision, manages the backlog, and prioritizes work.
- Scrum Master: Facilitates the Scrum process, removes obstacles, and ensures adhrerence to Scrum principles.
- Development team: Team responsible for delivering the product increment
- Artifacts
- Product backlog: Prioritized list of work for the team, maintained by the Product Owner
- Sprint backlog: Subset of items from the product backlog planned for the Sprint
- Increment: A working, shippable piece of the product at the end of a Sprint
- Cerimonies
- Sprint planning: Meeting to define the Sprint Goal and select backlog items for the Sprint
- Daily Scrum: Short daily meeting (15 min. max) to discuss progress, blockers, and plans
- What did you do yesterday?
- What will you do today?
- Are there any impediments?
- Sprint Review: End-of-Sprint demo or presentation of the increment to stakeholders
- Sprint Retrospective: Reflection on the Sprint to identify areas for improvements