Skip to content

Complete Integration

This guide shows how to combine authentication, rate limiting, and metrics in a complete endpoint. These features work together to provide secure, protected, and observable APIs.

Feature Integration Order

When combining features, they execute in a specific order:

graph LR
    A[Request] --> B[Authentication]
    B --> C[Rate Limiting]
    C --> D[Metrics]
    D --> E[Handler]
  1. Authentication - Validates the API key or user token
  2. Rate Limiting - Checks if the project has exceeded its quota
  3. Metrics - Records the request for billing
  4. Handler - Your business logic

Framework Examples

In FastAPI, combine features using the dependencies parameter:

"""Complete FastAPI integration example.

This example shows how to combine authentication, rate limiting, and metrics
in a FastAPI application. Based on real-world usage patterns.
"""

from functools import partial
from typing import Any

from fastapi import APIRouter, Depends, Request, Response

from application_kit.authenticator.types import Products
from application_kit.fastapi.metrics import AddJobToMetrics
from application_kit.fastapi.ratelimit import ProjectRateLimiter
from application_kit.fastapi.security import AuthenticateKey

# Create router for your API endpoints
router = APIRouter(prefix="/api/v1", tags=["locations"])

# Metrics: create a partial function for your service to simplify usage
add_job_to_metrics = partial(AddJobToMetrics, "LOCATIONS")


@router.get(
    "/search",
    response_class=Response,
    dependencies=[
        # Authentication validates the API key
        Depends(AuthenticateKey(product=Products.STORES)),
        # Rate limiting: max 100 requests per period
        Depends(ProjectRateLimiter(max_requests=100)),
        # Metrics: track this endpoint
        Depends(add_job_to_metrics("search")),
    ],
)
async def search_locations(request: Request, query: str) -> dict[str, Any]:
    """Search for locations.

    This endpoint demonstrates the complete integration:
    - Authentication validates the API key
    - Rate limiting prevents abuse
    - Metrics track usage for billing
    """
    # Access project reference from the validated token
    project_token = request.state.project_token
    project_ref = project_token.reference

    # Your business logic here
    return {
        "query": query,
        "project_id": project_ref.project_id if project_ref else None,
        "results": [],
    }


@router.post(
    "/locations",
    dependencies=[
        # Write permission required for POST
        Depends(AuthenticateKey(product=Products.STORES, write_permission_needed=True)),
        Depends(ProjectRateLimiter(max_requests=50)),
        Depends(add_job_to_metrics("create")),
    ],
)
async def create_location(request: Request) -> dict[str, Any]:
    """Create a new location.

    This endpoint requires write permission.
    """
    project_token = request.state.project_token
    project_ref = project_token.reference
    return {
        "status": "created",
        "project_id": project_ref.project_id if project_ref else None,
    }

Key Points:

  • Use dependencies=[] to apply middleware without requiring a return value
  • Create reusable dependency instances (like add_job_to_metrics) with functools.partial
  • Access the authenticated project via request.state.project_token

In Django, stack decorators in the correct order (bottom decorator executes first):

"""Complete Django integration example.

This example shows how to combine authentication, rate limiting, and metrics
in a Django application using decorators.
"""

from typing import Any

from django.http import HttpRequest, JsonResponse

from application_kit.authenticator.types import Products
from application_kit.django.decorators import authenticate_key, count_request
from application_kit.django.ratelimit import rate_limit
from application_kit.django.request import get_project_reference


@authenticate_key(product=Products.STORES)
@rate_limit(max_requests=100)
@count_request("LOCATIONS", "search")
def search_locations(request: HttpRequest) -> JsonResponse:
    """Search for locations.

    This endpoint demonstrates the complete integration:
    - @authenticate_key validates the API key
    - @rate_limit prevents abuse (100 requests per period)
    - @count_request tracks usage for billing

    Decorators are applied bottom-to-top, so the order is:
    1. count_request (always counts, even if rate limited)
    2. rate_limit (checks rate limit)
    3. authenticate_key (validates API key first)
    """
    # Access the project reference using the helper
    project_ref = get_project_reference(request)

    # Your business logic here
    query = request.GET.get("query", "")
    results: list[Any] = []

    return JsonResponse(
        {
            "query": query,
            "project_id": project_ref.project_id,
            "results": results,
        }
    )


@authenticate_key(product=Products.STORES, write_permission_needed=True)
@rate_limit(max_requests=50)
@count_request("LOCATIONS", "create")
def create_location(request: HttpRequest) -> JsonResponse:
    """Create a new location.

    This endpoint requires write permission.
    """
    project_ref = get_project_reference(request)

    return JsonResponse(
        {
            "status": "created",
            "project_id": project_ref.project_id,
        }
    )

Key Points:

  • Decorators are applied bottom-to-top, so @authenticate_key runs first (outermost)
  • Use get_project_reference(request) helper to access project IDs safely
  • The helper raises PermissionDenied if called without a valid token

Legacy

Django Ninja support is maintained for existing projects. For new projects, use FastAPI instead.

Django Ninja uses the @chain() and @terminate() pattern with Django decorators:

"""Complete Django Ninja integration example.

This example shows how to combine authentication, rate limiting, and metrics
in a Django Ninja application using the chain/terminate pattern.
"""

from django.http import HttpRequest

from application_kit.django.decorators import authenticate_key, count_request
from application_kit.django.ratelimit import rate_limit
from application_kit.django.request import get_project_reference
from application_kit.shinobi.api import WoosmapApi
from application_kit.shinobi.authentication import PrivateKeyAuth, PublicKeyAuth
from application_kit.shinobi.decorators import chain, terminate

api = WoosmapApi(description="Locations API")


@api.get("/search", auth=[PublicKeyAuth(), PrivateKeyAuth()])
@chain(authenticate_key(), rate_limit(max_requests=100), count_request("LOCATIONS", "search"))
@terminate()
def search_locations(request: HttpRequest, query: str) -> dict[str, object]:
    """Search for locations.

    This endpoint demonstrates the complete integration:
    - auth=[...] defines accepted authentication schemes
    - @chain() applies decorators: authenticate_key -> rate_limit -> count_request
    - @terminate() finalizes the decorator chain
    """
    project_ref = get_project_reference(request)

    return {
        "query": query,
        "project_id": project_ref.project_id,
        "results": [],
    }


@api.post("/locations", auth=[PrivateKeyAuth()])
@chain(
    authenticate_key(write_permission_needed=True),
    rate_limit(max_requests=50),
    count_request("LOCATIONS", "create"),
)
@terminate()
def create_location(request: HttpRequest) -> dict[str, object]:
    """Create a new location.

    This endpoint requires write permission (private key only).
    """
    project_ref = get_project_reference(request)

    return {
        "status": "created",
        "project_id": project_ref.project_id,
    }

Key Points:

  • Use auth=[...] to define accepted authentication schemes
  • Use @chain() to apply Django decorators in sequence
  • Always end with @terminate() to finalize the decorator chain
  • Access project info via get_project_reference(request)

Common Patterns

Different Rate Limits for Read vs Write

Apply stricter rate limits to write operations:

"""FastAPI rate limit patterns example.

Shows different rate limits for read vs write operations.
"""

from fastapi import APIRouter, Depends

from application_kit.authenticator.types import Products
from application_kit.fastapi.ratelimit import ProjectRateLimiter
from application_kit.fastapi.security import AuthenticateKey

# Use standard APIRouter - rate limit docs are auto-injected when included in app
router = APIRouter()


# Read endpoint: 100 requests/period
@router.get(
    "/items",
    dependencies=[
        Depends(AuthenticateKey(product=Products.STORES)),
        Depends(ProjectRateLimiter(max_requests=100)),
    ],
)
async def list_items() -> list[str]:
    """List all items."""
    return []


# Write endpoint: 10 requests/period with write permission
@router.post(
    "/items",
    dependencies=[
        Depends(AuthenticateKey(product=Products.STORES, write_permission_needed=True)),
        Depends(ProjectRateLimiter(max_requests=10)),
    ],
)
async def create_item() -> dict[str, str]:
    """Create a new item."""
    return {"status": "created"}
"""Django rate limit patterns example.

Shows different rate limits for read vs write operations.
"""

from django.http import HttpRequest, JsonResponse

from application_kit.authenticator.types import Products
from application_kit.django.decorators import authenticate_key
from application_kit.django.ratelimit import rate_limit


@authenticate_key(product=Products.STORES)
@rate_limit(max_requests=100)
def list_items(request: HttpRequest) -> JsonResponse:
    return JsonResponse({"items": []})


@authenticate_key(product=Products.STORES, write_permission_needed=True)
@rate_limit(max_requests=10)
def create_item(request: HttpRequest) -> JsonResponse:
    return JsonResponse({"status": "created"})
"""Django Ninja rate limit patterns example.

Shows different rate limits for read vs write operations.
"""

from django.http import HttpRequest

from application_kit.django.decorators import authenticate_key
from application_kit.django.ratelimit import rate_limit
from application_kit.shinobi.api import WoosmapApi
from application_kit.shinobi.authentication import PrivateKeyAuth, PublicKeyAuth
from application_kit.shinobi.decorators import chain, terminate

api = WoosmapApi(description="Items API")


# Read endpoint: 100 requests/period
@api.get("/items", auth=[PublicKeyAuth(), PrivateKeyAuth()])
@chain(authenticate_key(), rate_limit(max_requests=100))
@terminate()
def list_items(request: HttpRequest) -> list[str]:
    return []


# Write endpoint: 10 requests/period with write permission
@api.post("/items", auth=[PrivateKeyAuth()])
@chain(authenticate_key(write_permission_needed=True), rate_limit(max_requests=10))
@terminate()
def create_item(request: HttpRequest) -> dict[str, str]:
    return {"status": "created"}

Overriding Configuration in Tests

FastAPI uses the Settings object. Use SettingsOverride to override settings in tests:

"""FastAPI: Disabling rate limiting example.

Shows how to disable rate limiting via settings override in tests.
"""

from application_kit.fastapi.settings import SettingsOverride

# In tests, disable rate limiting:
with SettingsOverride(DISABLE_RATE_LIMIT=True):
    # Rate limiting is bypassed for all endpoints
    pass

Django uses get_configuration(). Use the test mixin or context manager:

"""Django: Disabling rate limiting in tests.

For Django, use the BenderSettingsOverrideTest mixin in test classes,
or OverrideContextManager for context manager usage.
"""

import unittest
from typing import Any

from application_kit import ApplicationKitConfig, configuration
from application_kit.configuration import OverrideContextManager
from application_kit.test_case import BenderSettingsOverrideTest


# Option 1: Test class mixin
class MyTestCase(BenderSettingsOverrideTest, unittest.TestCase):
    """Test case with configuration overrides."""

    def get_overrides(self) -> dict[ApplicationKitConfig, Any]:
        """Return configuration overrides for tests."""
        return {
            # Add your overrides here
        }

    def test_something(self) -> None:
        # Tests run with overrides applied
        pass


# Option 2: Context manager for granular control
def test_with_context_manager() -> None:
    """Use OverrideContextManager for specific test sections."""
    with OverrideContextManager(configuration):
        # Configuration overrides applied here
        pass

Next Steps