Skip to main content

Welcome to a Grasp lesson

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

Lesson 17

Shared Logic with FastAPI Depends

Recommended resources

Hello! Welcome back to our course on FastAPI.

In the last lesson, we took a big step toward building professional applications by using APIRouter to organize our endpoints into separate files. This made our project structure much cleaner and more scalable. However, organizing files is only part of the story. What about organizing and reusing the logic inside our functions?

You might notice that different endpoints sometimes need to perform the same initial steps—like validating a token, connecting to a database, or parsing common query parameters. Copying and pasting this logic is inefficient and error-prone. This lesson addresses that exact problem.

Our learning outcome is to: Create and use a simple dependency function (Depends) to share logic across endpoints. We will explore FastAPI's Dependency Injection system, a powerful feature that helps you write cleaner, more reusable, and more testable code.

1. The "Why": What is Dependency Injection?

Before we dive into the code, it's important to understand the core concept behind FastAPI's Depends system: Dependency Injection (DI). In programming, a "dependency" is something a piece of code needs to do its job. For example, a prediction endpoint depends on a loaded ML model. Dependency Injection is a design pattern where these dependencies are "injected" from an external source, rather than being created inside the function itself.

This leads to what's called "loose coupling," where components of your application are independent and can be easily changed or tested in isolation.

To get a clear, high-level overview of this concept, let's watch the first few minutes of this video.

Dependency Injection Explained Like You’re 5 (with FastAPI Examples)

This video from Eric Roby provides an excellent, beginner-friendly explanation of Dependency Injection, contrasting it with the 'tightly coupled' approach and explaining its benefits.

Watch the video from 00:45 to 04:43. Focus on understanding: The definition of Dependency Injection. What 'inversion of control' means. The difference between 'tightly coupled' and 'loosely coupled' applications.

As the video explained, the main goals of Dependency Injection are to:

  • Minimize code repetition.
  • Make code more modular and easier to test.
  • Decouple components so that changes in one place don't break others.

FastAPI has a first-class DI system built right in, and it's surprisingly simple to use.


2. The "How": Implementing Dependencies in FastAPI

The most common use case for dependencies is sharing logic that relies on parameters from the incoming request. Let's consider a classic example: pagination. Imagine you have multiple endpoints that return lists of items (e.g., users, products, predictions). You'll likely want to offer common query parameters like skip and limit on all of them.

Without dependencies, you would have to repeat these parameters in every function signature:

@app.get("/items/")
async def read_items(skip: int = 0, limit: int = 100):



    # ... logic ...

@app.get("/users/")
async def read_users(skip: int = 0, limit: int = 100):



    # ... logic ...

This is a clear violation of the "Don't Repeat Yourself" (DRY) principle. Let's fix it with a dependency.

Step 1: Create the Dependency Function

A dependency in FastAPI is just a regular Python function (it can be async def or def). This function can accept the same parameters as a path operation function (e.g., query parameters, path parameters, body payloads).

We'll create a function to handle our common pagination parameters.

async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}

This simple function defines the query parameters we want to share and returns them in a dictionary.

Step 2: "Depend" on the Function

Now, we can "inject" this logic into our endpoints using Depends from the fastapi module.

from fastapi import Depends, FastAPI

app = FastAPI()

async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}

@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons

@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

Here's what happens when a request comes to /items/:

  1. FastAPI sees the parameter commons with a default value of Depends(common_parameters).
  2. It calls the common_parameters function.
  3. It inspects the request for query parameters q, skip, and limit and passes them to common_parameters.
  4. common_parameters returns a dictionary (e.g., {"q": None, "skip": 0, "limit": 100}).
  5. FastAPI assigns this dictionary to the commons parameter in read_items.
  6. Your endpoint code runs with the commons variable now holding the resolved parameters.

The official documentation provides a clear, concise guide on this exact pattern.

Dependencies - FastAPI

The official FastAPI documentation is the best source for core concepts. This section will walk you through creating and using your first dependency.

Read the sections 'Create a dependency, or "dependable"' and 'Declare the dependency, in the "dependant"'. Notice how you just pass the function name to Depends without calling it.

The beauty of this system is its clarity and reusability. This is powerfully illustrated when used for more complex logic, like authentication.

FastAPI Auth with Dependency Injection: Good vs. Bad Implementation
This image compares an endpoint cluttered with authentication logic (Bad) versus a clean endpoint that injects the logic via a dependency (Good). This clearly shows how dependencies improve readability and maintainability.

3. Hands-On Practice: Creating a Shared Dependency

Let's put this into practice. We'll add two new "dummy" endpoints to our main.py and then refactor them to use a shared dependency.

Task:

  1. In your main.py file, add two new GET endpoints: /status and /monitoring.

  2. For both endpoints, add the same two optional query parameters: verbose: bool = False and level: int = 1.

  3. Each endpoint should simply return a dictionary containing the values of these parameters.

    • /status should return {"status": "OK", "verbose": ..., "level": ...}
    • /monitoring should return {"service": "monitoring", "verbose": ..., "level": ...}

    You'll notice you're repeating the verbose and level parameters.

  4. Now, refactor!

    • Create a single dependency function, let's call it common_debug_params, that defines the verbose and level parameters and returns them as a dictionary.
    • Import Depends from fastapi.
    • Modify both /status and /monitoring endpoints to use Depends(common_debug_params). The logic inside your endpoints will need to be adjusted to get the values from the dictionary returned by the dependency.
  5. Run the server and test both endpoints in your browser or Swagger UI (e.g., http://127.0.0.1:8000/status?verbose=true&level=2).

Click here for the solution

main.py:

from fastapi import FastAPI, Depends
from routers import predictions

app = FastAPI(
    title="House Price Prediction API",
    description="API to predict house prices using a simple model.",
    version="0.3.0"
)




# Include the router from the previous lesson
app.include_router(
    predictions.router,
    prefix="/predict",
    tags=["House Price Predictions"]
)




# --- New code for this lesson ---




# 1. The dependency function
async def common_debug_params(verbose: bool = False, level: int = 1):
    return {"verbose": verbose, "level": level}




# Main root endpoint
@app.get("/")
async def root():
    return {"message": "Welcome to the House Price Prediction API"}




# 2. Endpoints using the dependency
@app.get("/status", tags=["General"])
async def get_status(debug_params: dict = Depends(common_debug_params)):



    # The `debug_params` dict now contains {'verbose': ..., 'level': ...}
    response = {"status": "OK"}
    response.update(debug_params)
    return response

@app.get("/monitoring", tags=["General"])
async def get_monitoring_status(debug_params: dict = Depends(common_debug_params)):
    response = {"service": "monitoring"}
    response.update(debug_params)
    return response

4. Connecting to Your Goals: Dependencies for ML Models

While sharing query parameters is a great example, the true power of dependency injection will become apparent as we build more complex applications. For your goal of serving ML models, this pattern is essential.

Instead of loading a model into a global variable, the best practice is to load it once at startup and then use a dependency to "inject" it into your prediction endpoint.

Dependency Injection for Model Loading

This article from ApX Machine Learning directly addresses how to use dependency injection for ML models, which is central to your learning goals.

Read the sections 'Using Depends for Model Access' and 'Benefits of Using Dependency Injection for Models'. You don't need to implement this now, but focus on understanding the pattern: a get_model function returns a loaded model, which is then injected into the prediction endpoint.

As the article highlights, this approach makes your endpoint logic decoupled from the model loading logic, which makes it significantly more testable and maintainable. We will implement this exact pattern in a future module.


Conclusion

Excellent work! You've learned one of the most important concepts for building robust and scalable APIs with FastAPI.

Key Takeaways:

  • Dependency Injection (DI) is a pattern where an object or function receives its dependencies from an external source.
  • In FastAPI, you use Depends to declare a dependency in a path operation function's parameters.
  • A dependency is typically a function that encapsulates shared logic (e.g., parsing common parameters, authentication, database sessions).
  • Using DI leads to cleaner, more modular, and highly testable code by promoting the DRY (Don't Repeat Yourself) principle.

In this lesson, we saw how to run shared code for specific endpoints. In our next lesson, we will learn about middleware, which allows us to perform actions on every single request that comes into our application, such as adding logs or timing how long a request takes to process.