5 Must-Know Design Patterns for Building Scalable FastAPI Applications
Design patterns are reusable solutions to commonly occurring problems in software development. By using these patterns, developers can save time and effort by not having to solve the same problems repeatedly.
Design Patterns for FastAPI
we will cover several design patterns that can be applied to FastAPI. These patterns include:
- Singleton Pattern: ensures that only one instance of a class exists and provides a global point of access to it.
- Factory Pattern: provides a way to create objects without specifying the exact class of object that will be created.
- Observer Pattern: defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
- Strategy Pattern: allows you to define a family of algorithms, encapsulate each one, and make them interchangeable at runtime.
- Decorator Pattern: allows you to add behavior to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class.
Singleton Pattern
The Singleton Pattern is a creational design pattern that ensures that only one instance of a class exists and provides a global point of access to it. This pattern is useful when you need to ensure that there is only one instance of a class in your application, such as a database connection or a logging system.
To implement the Singleton Pattern:
class Singleton:
_instance = None
def __new__(cls):
if not cls._instance:
cls._instance = super().__new__(cls)
return cls._instance
you can define a private constructor and a static method that creates and returns the instance of the class. The static method should check if the instance has already been created, and if so, return it. If not, it should create a new instance and return it. The instance should be stored as a private static variable within the class
Here’s an example that might be used in the FastAPI
from fastapi import FastAPI, Depends
from pymongo import MongoClient
from typing import Optional
class Database:
instance = None
def __new__(cls):
if cls.instance is None:
cls.instance = super().__new__(cls)
cls.instance.client = MongoClient("mongodb://localhost:27017/")
cls.instance.db = cls.instance.client["mydatabase"]
return cls.instance
def get_db() -> Optional[Database]:
return Database()
app = FastAPI()
@app.get("/")
def read_root(db: Optional[Database] = Depends(get_db)):
if db:
result = db.db.my_collection.find_one()
return {"message": result}
else:
return {"message": "Failed to connect to database."}
In this example, we define a Database
class that creates a database connection using the PyMongo library. The Database
class uses the Singleton pattern to ensure that only one instance of the class is created during the lifetime of the application.
We then define a get_db
function that returns an optional instance of the Database
class. This function is used as a dependency in our FastAPI route handler by passing it as a parameter with the Depends
function.
When the read_root
function is called, the get_db
dependency is resolved and an instance of the Database
class is created if it doesn't already exist. The function then retrieves a single document from the my_collection
collection in the database and returns its contents.
By using the Singleton pattern in this way, we can ensure that we only create a single database connection throughout the lifetime of our FastAPI application, even when using dependency injection. This can help to reduce resource usage and improve performance, particularly for applications that require frequent database access.
Note that while this example uses the PyMongo library and get_db
function, you can use the Singleton pattern with any database library or dependency function in FastAPI.
Factory Pattern
The Factory Pattern is a creational design pattern that provides a way to create objects without specifying the exact class of object that will be created. This pattern is useful when you have a class hierarchy and you want to create objects of different classes based on some input, such as a user’s selection or a configuration parameter.
class ShapeFactory:
@staticmethod
def create_shape(shape_type):
if shape_type == "circle":
return Circle()
elif shape_type == "square":
return Square()
elif shape_type == "triangle":
return Triangle()
else:
raise ValueError("Invalid shape type")
To implement the Factory Pattern, you can define a factory method that creates and returns an instance of the appropriate class based on some input. This method should be static and defined in a separate factory class or in the base class of the class hierarchy.
Here’s an example of how you can use the Factory pattern in FastAPI to dynamically create objects based on user input
from fastapi import FastAPI, HTTPException
class PaymentProcessor:
def process_payment(self, amount: float):
pass
class CreditCardProcessor(PaymentProcessor):
def process_payment(self, amount: float):
return f"Processing credit card payment for {amount} dollars."
class PayPalProcessor(PaymentProcessor):
def process_payment(self, amount: float):
return f"Processing PayPal payment for {amount} dollars."
class PaymentProcessorFactory:
@staticmethod
def create_processor(processor_type: str) -> PaymentProcessor:
if processor_type == "credit_card":
return CreditCardProcessor()
elif processor_type == "paypal":
return PayPalProcessor()
else:
raise HTTPException(status_code=400, detail="Invalid payment processor type.")
app = FastAPI()
@app.post("/process_payment/{processor_type}")
def process_payment(processor_type: str, amount: float):
processor = PaymentProcessorFactory.create_processor(processor_type)
return processor.process_payment(amount)
In this example, we define a PaymentProcessor
abstract class that represents a payment processor, as well as two concrete implementations of the class: CreditCardProcessor
and PayPalProcessor
. We then define a PaymentProcessorFactory
class that uses the Factory pattern to dynamically create instances of the CreditCardProcessor
and PayPalProcessor
classes based on user input.
In our FastAPI application, we define a route handler for processing payments that takes a processor_type
parameter and an amount
parameter. The process_payment
function uses the PaymentProcessorFactory
to create an instance of the appropriate payment processor based on the processor_type
parameter, and then calls the process_payment
method on the created instance to process the payment.
By using the Factory pattern in this way, we can dynamically create objects based on user input without tightly coupling the code to the concrete implementations of the objects. This can help to make the code more modular and easier to maintain over time.
Observer Pattern
The Observer Pattern is a behavioral design pattern that defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. This pattern is useful when you have a set of objects that need to be updated when some other object changes, such as a model-view-controller architecture.
class Subject:
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def detach(self, observer):
self._observers.remove(observer)
def notify(self):
for observer in self._observers:
observer.update()
class Observer:
def update(self):
pass
To implement the Observer Pattern, you can define a subject class that maintains a list of its dependents and provides methods for adding and removing dependents. The subject class should also provide a method for notifying its dependents of a change. The dependents should implement an observer interface that defines a method for receiving updates from the subject.
Here’s an example of how you can use the Observer pattern in FastAPI to notify multiple subscribers of changes to a resource
from fastapi import FastAPI
from typing import List
class Subject:
def __init__(self):
self.observers = []
def register_observer(self, observer):
self.observers.append(observer)
def remove_observer(self, observer):
self.observers.remove(observer)
def notify_observers(self, message):
for observer in self.observers:
observer.update(message)
class Resource(Subject):
def __init__(self):
super().__init__()
self.data = []
def add_data(self, new_data):
self.data.append(new_data)
self.notify_observers(new_data)
class Subscriber:
def __init__(self, name):
self.name = name
def update(self, message):
print(f"{self.name} received message: {message}")
app = FastAPI()
resource = Resource()
@app.post("/add_data/{new_data}")
def add_data(new_data: str):
resource.add_data(new_data)
return {"message": "Data added."}
@app.post("/subscribe/{subscriber_name}")
def subscribe(subscriber_name: str):
subscriber = Subscriber(subscriber_name)
resource.register_observer(subscriber)
return {"message": f"{subscriber_name} subscribed."}
@app.post("/unsubscribe/{subscriber_name}")
def unsubscribe(subscriber_name: str):
subscriber = Subscriber(subscriber_name)
resource.remove_observer(subscriber)
return {"message": f"{subscriber_name} unsubscribed."}
In this example, we define a Subject
abstract class that represents a subject that can be observed, as well as a Resource
class that extends Subject
and represents a resource that can be modified. We also define a Subscriber
class that represents an observer of the resource.
Our FastAPI application defines three routes: /add_data
, /subscribe
, and /unsubscribe
. When a POST request is made to the /add_data
endpoint with a new data string, the add_data
function in our FastAPI application adds the new data to the Resource
and calls the notify_observers
method to notify all subscribers of the change.
When a POST request is made to the /subscribe
endpoint with a subscriber name string, the subscribe
function creates a new Subscriber
object with the provided name and registers it as an observer of the Resource
.
When a POST request is made to the /unsubscribe
endpoint with a subscriber name string, the unsubscribe
function creates a new Subscriber
object with the provided name and removes it as an observer of the Resource
.
By using the Observer pattern in this way, we can decouple the subject and observer objects and allow multiple subscribers to receive updates about changes to the resource. This can help to improve the modularity and maintainability of the code over time.
Strategy Pattern
The Strategy Pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. This pattern is useful when you have a set of algorithms that perform similar tasks but differ in their implementation, and you want to be able to switch between them easily.
class Sort:
def sort(self, data):
pass
class QuickSort(Sort):
def sort(self, data):
# Implementation of quicksort algorithm pass
class MergeSort(Sort):
def sort(self, data):
# Implementation of mergesort algorithm pass
class Client:
def init(self, sort_strategy):
self.sort_strategy = sort_strategy
def do_sort(self, data):
self.sort_strategy.sort(data)
To implement the Strategy Pattern, you can define an interface or abstract class that defines a method for performing the task, and then define a set of concrete classes that implement the interface or extend the abstract class. The client class should maintain a reference to an instance of the interface or abstract class and use it to perform the task.
Here’s an example of how you can use the Strategy pattern in FastAPI to encapsulate different algorithms and make them interchangeable
from fastapi import FastAPI
from typing import List
class SortStrategy:
def sort(self, data: List[int]) -> List[int]:
pass
class BubbleSort(SortStrategy):
def sort(self, data: List[int]) -> List[int]:
n = len(data)
for i in range(n):
for j in range(n-i-1):
if data[j] > data[j+1]:
data[j], data[j+1] = data[j+1], data[j]
return data
class QuickSort(SortStrategy):
def sort(self, data: List[int]) -> List[int]:
if len(data) <= 1:
return data
pivot = data[len(data)//2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
return self.sort(left) + middle + self.sort(right)
class Sorter:
def __init__(self, strategy: SortStrategy):
self.strategy = strategy
def set_strategy(self, strategy: SortStrategy):
self.strategy = strategy
def sort(self, data: List[int]) -> List[int]:
return self.strategy.sort(data)
app = FastAPI()
sorter = Sorter(BubbleSort())
@app.post("/sort/{strategy}")
def sort_data(strategy: str, data: List[int]):
if strategy == "bubble":
sorter.set_strategy(BubbleSort())
elif strategy == "quick":
sorter.set_strategy(QuickSort())
else:
return {"message": "Invalid sorting strategy."}
sorted_data = sorter.sort(data)
return {"sorted_data": sorted_data}
In this example, we define a SortStrategy
abstract class that represents a sorting strategy, as well as two concrete implementations of the class: BubbleSort
and QuickSort
. We then define a Sorter
class that encapsulates a SortStrategy
and provides a sort
method that delegates sorting to the encapsulated strategy.
Our FastAPI application defines a single route: /sort
. When a POST request is made to the /sort
endpoint with a sorting strategy string and a list of integers to sort, the sort_data
function in our FastAPI application creates a new Sorter
object with the appropriate sorting strategy based on the strategy string parameter, and then calls the sort
method on the Sorter
object to sort the data.
By using the Strategy pattern in this way, we can encapsulate different sorting algorithms and make them interchangeable, allowing us to easily switch between different algorithms without changing the code that uses them. This can help to improve the modularity and maintainability of the code over time.
Decorator Pattern
The Decorator Pattern is a structural design pattern that allows you to add behavior to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class. This pattern is useful when you have a set of objects that perform similar tasks but differ in their behavior, and you want to be able to add or remove behavior from an object without affecting the behavior of other objects from the same class.
class Component:
def operation(self):
pass
class ConcreteComponent(Component):
def operation(self):
return "ConcreteComponent"
class Decorator(Component):
def __init__(self, component):
self._component = component
def operation(self):
return self._component.operation()
class ConcreteDecoratorA(Decorator):
def operation(self):
return f"ConcreteDecoratorA({self._component.operation()})"
class ConcreteDecoratorB(Decorator):
def operation(self):
return f"ConcreteDecoratorB({self._component.operation()})"
To implement the Decorator Pattern, you can define a base class or interface that defines the methods and properties of the objects. You can then define a set of concrete classes that implement the base class or interface and add behavior to the objects. Finally, you can define a set of decorator classes that wrap the concrete classes and add or remove behavior from the objects.
Here’s an example of how you can use the Decorator pattern in FastAPI to add functionality to an existing endpoint
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello, world!"}
def add_greeting(original_function):
def new_function():
return {"greeting": "Welcome to my FastAPI application!"} | original_function()
return new_function
@app.get("/decorated")
@add_greeting
def read_decorated():
return {"data": "This is some data."}
In this example, we define a basic FastAPI application with a route handler for the root path (/
) that returns a simple message.
We then define a add_greeting
function that takes an existing function as a parameter and returns a new function that adds a greeting message to the output of the original function. The new function uses the |
operator to merge the output of the original function with the new greeting message.
We then define a new endpoint (/decorated
) that is decorated with the @add_greeting
decorator. This decorator adds the greeting message to the output of the read_decorated
function, which returns some sample data.
When a GET request is made to the /decorated
endpoint, the add_greeting
function is called to add the greeting message to the output of the read_decorated
function, and the resulting output is returned as the response.
By using the Decorator pattern in this way, we can add functionality to an existing endpoint without modifying the original function or the code that calls it. This can help to improve the modularity and maintainability of the code over time.
Conclusion
In this series, we have covered several design patterns that can be applied to FastAPI. the Singleton, Factory, Observer, Strategy, and Decorator Patterns are all useful design patterns that can be applied to FastAPI. By understanding these patterns and when to use them, you can write more modular, reusable, and maintainable code.
We hope that this series has been helpful in your journey to becoming a better FastAPI developer. Thanks for reading!