Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 39

Securing Routes: Dependency Injection vs. DRF Permissions

Recommended resources

Hello! Welcome to the next lesson in our FastAPI journey.

In the last lesson, we built the get_current_user dependency, a crucial piece of security middleware that can extract and validate a JWT from a request. We now have the keymaker and the lock inspector. Today, we'll put that inspector to work, securing our application's endpoints.

Our goal is to protect routes by injecting the current authenticated user via a dependency, comparing it to DRF's permission_classes. By the end of this lesson, you'll be able to lock down endpoints, perform ownership checks, and understand how FastAPI's dependency injection system creates a flexible and explicit alternative to the permission patterns you're familiar with from Django REST Framework.

1. The FastAPI Way vs. The Django Way

Let's start with a direct comparison you requested. In Django REST Framework, you protect a view by adding a permission_classes attribute:




# Django REST Framework
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

class MyProtectedView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, format=None):
        content = {'message': f'Hello, {request.user.username}!'}
        return Response(content)

The IsAuthenticated class does the work behind the scenes. If the user is authenticated, the framework makes the user object available on the request object.

FastAPI achieves the same result with a different philosophy: explicit dependency injection instead of implicit context on a request object.




# FastAPI
from fastapi import APIRouter, Depends
from my_auth_logic import get_current_user, User # Assuming these are defined

router = APIRouter()

@router.get("/me")
async def read_users_me(current_user: User = Depends(get_current_user)):
    return {"username": current_user.username}

Here, Depends(get_current_user) is the direct equivalent of permission_classes = [IsAuthenticated]. It ensures the user is authenticated before the function body runs. Instead of finding the user on a magic request object, it's explicitly passed as a typed function argument, current_user, which offers better editor support and static analysis.

The following article provides a succinct summary of this difference.

Django vs FastAPI: I Built the Same App Twice - Medium

This article, 'Django vs FastAPI: I Built the Same App Twice', has a great section comparing dependency injection styles. It will help frame the conceptual shift.

First, read the subsection titled 'Speed Factor 3: Dependency injection that doesn’t suck'. Then, scroll down to the section titled 'Dependency Injection' under the 'Code-to-Code Comparison'. Notice how the FastAPI example explicitly declares its dependencies (user: User = Depends(...)) in the function signature, whereas Django uses decorators and middleware.


2. Implementing Route Protection and Ownership

Now, let's see this in action. The best way to learn this is to watch a practical implementation. The following video by Corey Schafer perfectly demonstrates how to secure CRUD endpoints for a blog post resource.

We will walk through the key steps shown in the video.

Python FastAPI Tutorial (Part 11): Authorization - Protecting Routes and Verifying Current User

This video is a masterclass in applying the concepts we're discussing. You'll see how to protect routes, use the injected user, and handle ownership permissions.

Please watch the segment from 00:02:29 to 00:12:35. It's about 10 minutes, but it covers the entire workflow from creating the dependency to implementing ownership checks. Pay close attention to the following points: The structure of the get_current_user dependency. How adding it to the create_post function secures the endpoint. How the current_user.id is used to assign post ownership. The logic for checking ownership in the update and delete functions. The explanation of 401 Unauthorized vs. 403 Forbidden.

Let's break down the key patterns from the video.

Step 1: Protecting a "Create" Endpoint

As you saw in the video, protecting the create_post endpoint was as simple as adding the current_user dependency:




# In your posts router file
from ..auth import get_current_user # Our dependency
from .. import schemas




# A modern way to create a reusable dependency alias using Annotated
CurrentUser = Annotated[schemas.User, Depends(get_current_user)]

@router.post("/", status_code=status.HTTP_201_CREATED, response_model=schemas.PostResponse)
async def create_post(post: schemas.PostCreate, db: DbSession, current_user: CurrentUser):



    # Because of `Depends(get_current_user)`, this code only runs for authenticated users.
    



    # Create the new post, associating it with the logged-in user
    new_post = models.Post(author_id=current_user.id, **post.model_dump())
    
    db.add(new_post)
    db.commit()
    db.refresh(new_post)
    
    return new_post

Key Improvements:

  • Security: The endpoint is now locked. Unauthenticated requests receive a 401 Unauthorized error before your code even runs.
  • Data Integrity: The author_id is derived from the trusted JWT, not from the request body. A user can no longer create a post on behalf of someone else by manipulating the JSON payload.

Step 2: Checking Ownership for "Update" and "Delete"

This is where FastAPI's approach really shines in its clarity. To ensure a user can only edit their own posts, we add a simple check inside the path operation function.

@router.put("/{id}", response_model=schemas.PostResponse)
async def update_post(id: int, updated_post: schemas.PostCreate, db: DbSession, current_user: CurrentUser):
    post_query = db.query(models.Post).filter(models.Post.id == id)
    post = post_query.first()

    if post is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Post with id: {id} does not exist")




    # The permission check!
    if post.author_id != current_user.id:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to perform requested action")

    post_query.update(updated_post.model_dump(), synchronize_session=False)
    db.commit()
    return post_query.first()

This is the direct equivalent of implementing a custom permission class in DRF, like IsOwnerOrReadOnly.

  • In DRF, you would write a class with a has_object_permission method.
  • In FastAPI, the logic is often placed directly in the endpoint for simplicity. The if post.author_id != current_user.id: check is your has_object_permission logic.

As the video explained, the choice of status code is important:

  • 401 Unauthorized: The user is not logged in (invalid or missing token). The get_current_user dependency handles this for us.
  • 403 Forbidden: The user is logged in, but they are not allowed to perform this specific action (e.g., editing someone else's post). We raise this manually after our ownership check.

3. The Power of Reusability: Cleaning up with Dependencies

Dependencies are designed to be reusable. A great example is the common /users/me endpoint, which should return the profile of the logged-in user.

Without a dependency, you might have to decode the token inside the endpoint. With our get_current_user dependency, the endpoint becomes trivial.

Python FastAPI Tutorial (Part 11): Authorization - Protecting Routes and Verifying Current User

Let's revisit the Corey Schafer video to see a powerful refactoring example.

Watch the short segment from 00:12:35 to 00:14:12. See how a me endpoint with nearly 30 lines of code is reduced to just 3 lines by leveraging the get_current_user dependency. This perfectly illustrates the Don't Repeat Yourself (DRY) principle in action.

The final code is beautifully simple and declarative:

@router.get("/me", response_model=schemas.UserResponse)
async def get_me(current_user: CurrentUser):



    # The dependency does all the work of finding and validating the user.
    # We just have to return it.
    return current_user

The official FastAPI documentation also presents this exact pattern as the canonical way to handle fetching the current user, solidifying its importance. You can review it in the "Get Current User" tutorial page, specifically the sections "Create a get_current_user dependency" and "Inject the current user".


4. Comparison to DRF permission_classes

Let's summarize the comparison:

Feature Django REST Framework FastAPI
Authentication authentication_classes = [JWTAuthentication] A dependency like Depends(oauth2_scheme) is used within another dependency.
Basic Permission permission_classes = [IsAuthenticated] The primary dependency Depends(get_current_user) is added to the path operation.
Object-Level Permission permission_classes = [IsOwnerOrReadOnly] (custom class with has_object_permission) An explicit if obj.owner_id != current_user.id: check inside the path operation.
Accessing the User Implicitly available via request.user. Explicitly injected as a typed function argument current_user: User.

FastAPI's approach trades the "magic" of DRF's class attributes and implicit request properties for the explicitness and type-safety of dependency injection. This often makes the code easier to trace, test, and understand for developers who are new to the codebase.


Conclusion

You have now mastered a fundamental aspect of building secure FastAPI applications. You can not only lock down your routes but also implement fine-grained, object-level permissions.

Key Takeaways:

  • You protect a route by adding a dependency (e.g., Depends(get_current_user)) to its signature. This is the FastAPI equivalent of DRF's permission_classes = [IsAuthenticated].
  • The authenticated user object is injected directly into your path operation as a function argument, making your business logic clean and your code self-documenting.
  • Object-level permissions (ownership checks) are typically implemented with a simple if condition inside the path operation, which is analogous to DRF's has_object_permission method.
  • Remember to use 401 Unauthorized for authentication failures and 403 Forbidden for permission failures.

Preview of the Next Lesson:

We've handled authentication (IsAuthenticated) and ownership (IsOwner). What about more complex scenarios, like IsAdminUser? In our next lesson, we will implement role-based access control (RBAC) using dependencies that check user permissions. We'll build on today's concepts to create dependencies that can verify user roles, allowing you to create powerful and flexible authorization systems.