Structural patterns focus on how classes and objects are composed to form larger structures, ensuring that if one part changes, the entire structure doesn’t need to.

Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together by converting the interface of a class into another interface clients expect.

Use Cases in FastAPI

  • Third-party Service Integration: Adapting third-party libraries to fit your application’s interface.
  • Database Abstraction Layers: Providing a uniform interface over different database technologies.
  • API Version Compatibility: Allowing different API versions to coexist with uniform access.

Advantages

  • Enhances code reusability by allowing classes with incompatible interfaces to work together.
  • Promotes flexibility and interchangeability of components.

Disadvantages

  • Can increase the complexity of the codebase.
  • May lead to over-abstraction if not used judiciously.

Adapter Pattern with FastAPI: Third-party Service Integration

Suppose you want to integrate a third-party payment service with a different interface.

from abc import ABC, abstractmethod

# External Payment Service with its own interface
class ExternalPaymentService:
  def make_payment(self, amount: float, currency: str):
      return f"Paid {amount} {currency} using ExternalPaymentService"

# Abstract Payment Interface
class PaymentInterface(ABC):
  @abstractmethod
  def pay(self, amount: float) -> str:
      pass

# Adapter
class ExternalPaymentAdapter(PaymentInterface):
  def __init__(self, external_service: ExternalPaymentService):
      self.external_service = external_service

  def pay(self, amount: float) -> str:
      # Assume default currency is USD
      return self.external_service.make_payment(amount, "USD")

# FastAPI Route
app = FastAPI()

@app.post("/pay/")
async def make_payment(amount: float):
  external_service = ExternalPaymentService()
  payment_adapter = ExternalPaymentAdapter(external_service)
  result = payment_adapter.pay(amount)
  return {"status": "success", "message": result}

Facade Pattern

The Facade pattern provides a simplified interface to a complex subsystem, making it easier to use.

Use Cases in FastAPI

  • Simplifying Complex Subsystems: Providing a unified API over multiple services or modules.
  • Service Layer Implementation: Encapsulating business logic and exposing it through a simplified interface.
  • External API Integration: Abstracting interactions with multiple external APIs.

Advantages

  • Simplifies interactions with complex subsystems.
  • Reduces dependencies between clients and subsystems.
  • Improves code readability and maintainability.

Disadvantages

  • Can become a god object if not carefully designed.
  • May hide essential details, making debugging harder.

Facade Pattern with FastAPI: Service Layer

Assume you have multiple services (e.g., UserService, EmailService) and you want to provide a unified interface.

# Improved version with better adherence to SRP and DIP

from typing import List
from fastapi import FastAPI, Depends

# Services
class UserService:
  async def create_user(self, user_data: dict) -> dict:
      # Logic to create user
      return {"id": 1, "name": user_data["name"], "email": user_data["email"]}

class EmailService:
  async def send_welcome_email(self, user: dict):
      # Logic to send email
      print(f"Sending welcome email to {user['email']}")

class NotificationService:
  def __init__(self, email_service: EmailService):
      self.email_service = email_service

  async def notify_user_creation(self, user: dict):
      await self.email_service.send_welcome_email(user)

# Facade with dependency injection
class UserFacade:
  def __init__(self, user_service: UserService, notification_service: NotificationService):
      self.user_service = user_service
      self.notification_service = notification_service

  async def register_user(self, user_data: dict) -> dict:
      user = await self.user_service.create_user(user_data)
      await self.notification_service.notify_user_creation(user)
      return user

# FastAPI Route with dependency injection
app = FastAPI()

@app.post("/register/")
async def register(user_data: dict, facade: UserFacade = Depends(UserFacade)):
  user = await facade.register_user(user_data)
  return {"status": "success", "user": user}

Decorator Pattern

The Decorator pattern allows behavior to be added to individual objects, dynamically, without affecting the behavior of other objects from the same class.

Use Cases in FastAPI

  • Route Middleware: Adding pre-processing or post-processing to routes.
  • Authentication/Authorization: Wrapping routes with security checks.
  • Request/Response Transformation: Modifying or enriching incoming and outgoing data.

Advantages

  • Enhances functionality without modifying existing code.
  • Promotes code reuse and separation of concerns.
  • Flexible addition and removal of behaviors.

Disadvantages

  • Can lead to complex and hard-to-follow code if overused.
  • May obscure the core logic of functions.

Decorator Pattern with FastAPI: Authentication Creating a decorator to enforce authentication on specific routes.

from fastapi import FastAPI, HTTPException, Request
from functools import wraps
from typing import Callable

app = FastAPI()

def authenticate(func: Callable):
  @wraps(func)
  async def wrapper(*args, **kwargs):
      request: Request = kwargs.get('request')
      token = request.headers.get("Authorization")
      if token != "Bearer validtoken":
          raise HTTPException(status_code=401, detail="Unauthorized")
      return await func(*args, **kwargs)
  return wrapper

@app.get("/protected/")
@authenticate
async def protected_route(request: Request):
  return {"message": "You are authenticated"}

Using FastAPI Dependencies as Decorators

While FastAPI’s dependency injection system often replaces the need for traditional decorators, you can still combine both for more complex scenarios.

from fastapi import Depends

def check_permissions(required_role: str):
  def decorator(func: Callable):
      async def wrapper(*args, **kwargs):
          user = kwargs.get('current_user')
          if user.get("role") != required_role:
              raise HTTPException(status_code=403, detail="Forbidden")
          return await func(*args, **kwargs)
      return wrapper
  return decorator

def get_current_user():
  # Dummy implementation
  return {"username": "johndoe", "role": "admin"}

@app.get("/admin/")
@check_permissions("admin")
async def admin_route(current_user: dict = Depends(get_current_user)):
  return {"message": "Welcome, admin!"}