In the age of AI programming, there is one kind of code that gets copied around with abandon:

logger.info("Start processing request")

if not token:
    raise UnauthorizedError()

start = time.time()
result = await service.do_something()
logger.info(f"Execution time: {time.time() - start}")
return result

At first there is only one endpoint, and it looks harmless enough.

But once the project grows to dozens or even hundreds of endpoints, things quickly become:

User endpoints
  ├─ Authentication
  ├─ Logging
  ├─ Parameter validation
  ├─ Business logic
  └─ Instrumentation

Agent endpoints
  ├─ Authentication
  ├─ Logging
  ├─ Parameter validation
  ├─ Business logic
  └─ Instrumentation

Knowledge base endpoints
  ├─ Authentication
  ├─ Logging
  ├─ Parameter validation
  ├─ Business logic
  └─ Instrumentation

  ...

The only truly different part is the few lines of business logic in the middle. As for the rest, every time the AI writes another endpoint, it dutifully copies it all over again.

This is exactly the problem that AOP (Aspect-Oriented Programming) set out to solve.


📌 Technology Card

AOP / Aspect-Oriented Programming

A software design philosophy that extracts logic shared by many business modules out of the specific business code and handles it in one place.

This shared logic is commonly referred to as:

Cross-Cutting Concerns

Typical examples include: logging, authentication, authorization, auditing, performance metrics, tracing, caching, transactions, instrumentation, and exception handling.

The core of AOP is not any particular framework or special syntax, but a very simple architectural principle:

Business code handles business; shared rules handle shared rules.


💡 An Intuitive Way to Understand It: AOP Is Like Airport Security

Imagine an airport with many boarding gates.

The Beijing flight has one gate:

Security check
ID check
Register information
Board the plane

The Shanghai flight is the same:

Security check
ID check
Register information
Board the plane

If we followed the approach many AIs take when writing code, it would probably turn into:

Every gate gets its own security checkpoint equipment.

Obviously unreasonable. What happens in the real world is:

flowchart TB A[Passenger request] --> B[Unified entrance] subgraph CROSS["Unified security (cross-cutting logic)"] B --> C[Identity check] C --> D[Security screening] D --> E[Information registration] end E --> F[Boarding area] F --> G[Beijing flight] F --> H[Shanghai flight] F --> I[Guangzhou flight]

All passengers go through the unified security checkpoint first, and then proceed to their own gates.

Software is no different.

Without cross-cutting governance:

Router A
 ├─ Logging
 ├─ Token validation
 ├─ Permission check
 ├─ Business code
 └─ Performance metrics

Router B
 ├─ Logging
 ├─ Token validation
 ├─ Permission check
 ├─ Business code
 └─ Performance metrics

After extraction:

flowchart TB A[Request] --> B["Middleware
Logging / Request ID
Performance / Audit"] B --> C["Authentication
Identity / Permissions"] C --> D[Router] D --> E[Service] E --> F[Repository]

The Router can then refocus on a single job:

Handling business.


💡 Don’t Mistake AOP for “You Must Use an AOP Framework”

When many developers first encounter AOP, they think of Spring AOP in Java.

For example:

@Before
@After
@Around

But that is just one implementation of AOP. What truly matters about AOP is:

Identifying cross-cutting concerns and extracting them from business code.

In a Python + FastAPI project, you can perfectly well use:

Middleware
Dependency Injection
Decorator
ContextVar
Unified exception handling

to achieve the same architectural goal. For example, miniagent currently takes exactly this approach.

The core directories of the repository already clearly place general-purpose capabilities in app/core, which contains:

core/
├── audit_context.py
├── deps.py
├── logger_config.py
├── security/
│   ├── auth_permission.py
│   ├── jwt_auth.py
│   └── ...
└── service_container.py

rather than scattering authentication, logging, and audit logic across every business Router.

This is, in fact, a very Python/FastAPI-flavored realization of AOP thinking.


1. Architectural Rules: Where Should Cross-Cutting Logic Live?

For a FastAPI project, you can give the AI a very simple decision standard.

Global request-level logic → Middleware

Middleware is suitable for concerns that every HTTP request may involve.

For example:

Request logging
Request ID
Endpoint latency
Global auditing
Tracing
Unified headers

The basic structure:

@app.middleware("http")
async def middleware(request, call_next):

    # Before the request
    ...

    response = await call_next(request)

    # After the request
    ...

    return response

The idea behind it is:

           Before
Request → Middleware → Router
            After

That is, the classic AOP pattern of:

Uniformly inserting shared behavior before and after business logic executes.


2. The Real Implementation in miniagent

The diagram below fairly completely illustrates the AOP approach of miniagent:

miniagent AOP architecture

In the current main.py, after a request enters the unified logging middleware, a request_id is created, the start time is recorded, and an audit context is established — and only then is the actual Router invoked. After the request completes, the status code, latency, and audit results are logged uniformly, and X-Request-ID is returned to the client.

At the same time, AuthPermission centrally handles JWT verification, user resolution, user status checks, permission caching, and permission decisions, and writes identity information into the audit context.

And audit_context.py uses a ContextVar to hold the current request’s:

request_id
method
path
ip_address
user_id
username
change_count

so that this information propagates along the async call chain without being passed down layer by layer as function parameters.

Together, these three components form:

Middleware
    +
Dependency / Permission
    +
ContextVar

a three-layer cross-cutting system.

This is not the “heavyweight AOP” traditionally implemented via complex proxy mechanisms.

Instead, it fits the character of a FastAPI project better:

Implementing AOP thinking with the framework’s native capabilities.

1. Logging Is Not Written by Every Endpoint

In miniagent’s main.py there is a unified HTTP request logging middleware:

@app.middleware("http")
async def log_requests(request: Request, call_next):
    """Record all HTTP requests and inject request_id into every log line."""

    request_id = str(uuid4())
    request.state.request_id = request_id

    start_time = time.time()

    with logger.contextualize(request_id=request_id):

        logger.info(
            f"📥 {request.method} {request.url.path}"
        )

        response = await call_next(request)
        process_time = time.time() - start_time

        logger.info(
            f"📤 {request.method} {request.url.path} "
            f"- {response.status_code} ({process_time:.3f}s)"
        )

        response.headers["X-Process-Time"] = str(process_time)
        response.headers["X-Request-ID"] = request_id

        return response

This is an excerpt based on the current repository code; the actual implementation also handles exceptions, auditing, and login logging.

The change it brings is significant. Before, you might have:

@router.get("/users")
async def list_users():

    logger.info("GET /users start")
    start = time.time()

    result = await user_service.list_users()

    logger.info(
        f"GET /users finished: {time.time() - start}"
    )

    return result

Now the Router can simply be:

@router.get("/users")
async def list_users():
    return await user_service.list_users()

Because questions like:

Who made the request?
Which endpoint was accessed?
When did it start?
When did it end?
What was the status code?
How long did it take?
What is the Request ID?

no longer belong to the Router — the Middleware takes care of them uniformly.

This is what it means to:

Cut cross-cutting logic out of business code.


2. Identity and Permissions → Dependency

Not all cross-cutting logic is suitable for Middleware.

For example:

/user/profile

only requires being logged in.

Whereas:

/admin/users

may require the system:user:list permission.

Or, for instance:

/admin/users/{id}

may require the system:user:delete permission when deleting a user.

Clearly, you can’t simply have one global Middleware decide every business permission.

That’s where FastAPI’s:

Dependency Injection

comes in, i.e.:

Depends(...)

3. Authentication: Not Every Router Re-Parses JWT

miniagent implements a unified AuthPermission, responsible for:

JWT verification
Resolve the user
Check user status
Load the permission set
Permission check

JWT (JSON Web Token) is a common token format for authentication.

Rather than having every endpoint write its own:

token = request.headers.get("Authorization")
username = verify_token(token)
user = await find_user(username)
permissions = await load_permissions(user.id)

if permission not in permissions:
    raise HTTPException(...)

miniagent encapsulates this whole flow inside AuthPermission.

For example, the core permission-check logic is very clean:

async def check(
    self,
    user_id: int,
    required: str,
) -> None:

    perms = await self.get_permissions(user_id)

    if SUPER_PERMISSION in perms or required in perms:
        return

    raise_forbidden(
        "auth.permission_denied",
        required=required,
    )

The permission data itself is also cached, avoiding a fresh database read on every request.

This way, the business layer only needs to express:

What permission this endpoint requires.

Instead of re-implementing:

How the permission system actually works.


4. Declare Rules, Don’t Repeatedly Implement Them

This is a very important point of AOP thinking.

Good business code should be as close as possible to:

@router.delete("/users/{user_id}")
async def delete_user(...):
    return await user_service.delete(user_id)

while declaring, via a Dependency, Decorator, or other unified mechanism:

Requires login
Requires system:user:delete permission

instead of:

@router.delete("/users/{user_id}")
async def delete_user(...):

    # Parse Authorization
    ...

    # Verify JWT
    ...

    # Look up user
    ...

    # Look up permissions
    ...

    # Check super admin
    ...

    # Write access log
    ...

    # Record start time
    ...

    # ===== The real business finally begins =====

    await user_service.delete(user_id)

    # ===== Business ends =====

    # Write audit log
    ...

    # Write timing log
    ...

    return ...

The biggest danger of the latter is not the code length.

It is:

AI is exceptionally good at copying this kind of code.

And then copying it dozens of times.


5. Wrapping Permissions as a Callable Dependency

AuthPermission also contains a design well worth borrowing for AI projects:

class Permission:

    def __init__(
        self,
        permission_code: str,
    ) -> None:
        self._code = permission_code

    async def __call__(
        self,
        request: Request,
        credentials = Depends(_bearer_scheme),
    ) -> int:

        auth = request.app.state.container.auth

        user_id = await auth.resolve_user_id(
            credentials.credentials
        )

        await auth.check(
            user_id,
            self._code
        )

        return user_id

This way, the permission component itself becomes a dependency object FastAPI can recognize.

The whole relationship can be understood as:

Router
  │ Declares the permission
Permission("system:user:delete")
AuthPermission
  ├── Verify JWT
  ├── Resolve User
  ├── Load Permission
  └── Check Permission

The Router doesn’t need to understand:

How is a JWT parsed?
How is the user looked up?
Where is the permission cache?
How is a super admin determined?

It is only responsible for expressing:

I want system:user:delete

This is a fundamentally important architectural idea:

The business layer declares intent; the infrastructure layer implements the mechanism.


6. ContextVar Solves Cross-Layer Context Passing

Logging and auditing face another troublesome problem.

Suppose the call chain is:

HTTP Request
Router
Service
Repository
Database

If every layer needs:

request_id
user_id
username

The most straightforward approach might be:

service.run(
    request_id=request_id,
    user_id=user_id,
    ...
)

And then:

repository.save(
    request_id=request_id,
    user_id=user_id,
    ...
)

This quickly pollutes every function signature.

miniagent uses the following for its audit context:

ContextVar (Context Variable)

The code defines:

_audit_context: ContextVar[
    Optional[AuditRequestContext]
] = ContextVar(
    "audit_request_context",
    default=None
)

The context is established when the request enters:

begin_audit_context(...)

And when the request ends:

reset_audit_context(...)

After authentication succeeds, it calls:

set_audit_user(
    user.id,
    user.username
)

to attach the user identity to the audit context of the current request.

So the whole flow becomes:

            HTTP Request
           Middleware
                 ├── request_id
                 ├── method
                 ├── path
                 └── ip
           Audit Context
            AuthPermission
                 ├── user_id
                 └── username
           Audit Context
        Service / Repository

This is another kind of cross-cutting capability:

The request context propagates along the entire async call chain without polluting business function parameters.


3. What Are the Actual Benefits of AOP?

1. Less Duplicate Code

The most direct change:

100 endpoints

no longer means:

100 copies of logging code
100 copies of auth code
100 copies of timing code
100 copies of audit code

Shared rules are implemented exactly once.


2. Changing a Rule Means Changing It in One Place

Suppose the logs later need to add:

request_id
client_ip
user_id
latency

If logging is scattered across 200 endpoints:

Cost of change ≈ 200 edits

With Middleware:

Cost of change ≈ 1 edit

This is the problem architecture truly solves.


3. Business Code Becomes Easier to Read

A good Router should let you see at a glance:

What request it accepts
Which Service it calls
What result it returns

instead of first wading through dozens of lines of:

token
logger
permission
cache
metrics
audit

before finding the actual business.


4. Shared Rules Stay Consistent

If you let the AI write authentication 50 times, you may end up with 50 subtly different versions.

For example:

if permission not in permissions:

Another one:

if required_permission not in permissions:

Another forgets the super admin:

if required not in permissions:

Another forgets to check whether the user is disabled.

One of the values of AOP is:

Giving every shared rule a single authoritative implementation.


4. Establish Explicit AOP Architecture Rules for the AI

You can write the following directly into your Project Rules:

## Cross-Cutting Concerns

Logging, authentication, authorization, auditing, metrics, tracing, and exception handling are cross-cutting concerns.

It is forbidden to implement this logic repeatedly in Routers or business Services.

Unified rules:

- Global HTTP request logic uses Middleware;
- Authentication and permission checks prefer FastAPI Depends;
- Use Decorators only when dependency injection is unsuitable;
- Request-scoped context uses ContextVar;
- Exception and response conversion uses global exception handlers;

Routers are only responsible for request orchestration;
Services are only responsible for business logic;
Repositories are only responsible for data access.

These few lines of rules constrain the AI far more effectively than:

Please write high-quality code

Don’t Tell the AI to “Add Some Auth”

If you simply say:

Add permission control to this endpoint.

The AI will very likely invent a permission system on the spot. A better prompt is:

Please add permission control to this endpoint.

Requirements:

1. Do not parse JWT in the Router;
2. Do not re-implement permission checks;
3. Use the project's existing AuthPermission;
4. Perform the permission check via FastAPI Depends or the existing Permission mechanism;
5. The Router only declares the required permission;
6. Reuse the existing auth singleton in the ServiceContainer;
7. Do not alter the existing unified exception system.

This effectively tells the AI:

Do not invent mechanisms; only use the mechanisms the project already has.


Logging Requirements Should Be Given to the AI the Same Way

Bad prompt:

Add access logging to every endpoint.

The AI will very likely modify dozens of Routers directly:

logger.info(...)

Better prompt:

We need to add HTTP request access logging.

First determine whether this feature is a cross-cutting concern.

Requirements:

1. Do not modify Routers one by one;
2. Implement it uniformly with FastAPI Middleware;
3. Generate a request_id for every request;
4. Record method, path, status_code, and latency;
5. Return X-Request-ID in the response;
6. The logging context should propagate automatically to other modules within the current request;
7. Do not pollute business parameters in Services and Repositories.

This sentence:

First determine whether this is a cross-cutting concern

is well worth adding to your AI project rules.


Don’t Let Instrumentation Turn Business Code into a “Christmas Tree”

Many systems later add:

User clicks
Agent invocation counts
LLM token consumption
Endpoint latency
Knowledge base hit rate
Tool success rate

If all of it is written inline in the business code:

metrics.inc("agent_call")
logger.info(...)
tracer.start(...)
audit.record(...)
result = await agent.run()
metrics.observe(...)
logger.info(...)
tracer.end(...)

in the end, the actual business logic shrinks to:

result = await agent.run()

Code like this still runs, but it shows the cross-cutting logic has intruded too deeply into the business.

The healthier direction looks like:

Metrics
Logging
Tracing
Audit
   │ Cutting across horizontally
────────────────────
 Router → Service
────────────────────

rather than:

Router
Metrics
Logging
Tracing
Auth
Audit
Business

Summary

AOP is not mysterious. The essential problem it solves is:

Extracting the code that is “needed everywhere, but doesn’t truly belong to any single business” out of the business logic.

Logging, authentication, auditing, instrumentation, performance metrics, and tracing are all classic cross-cutting concerns.

For a FastAPI project, there is no need to introduce a complex framework just to chase the name “AOP”.

A very practical combination is enough:

Middleware
    +
Dependency Injection
    +
Decorator
    +
ContextVar
    +
Global Exception Handler

miniagent currently follows exactly this direction.

And in AI programming, we should go one step further and write it down as an explicit project rule:

When you discover duplicated cross-cutting logic, do not keep copying. Stop first and judge whether it should be extracted into Middleware, a Dependency, a Decorator, or unified infrastructure.

Ultimately, what we really want from AI is not:

Every file looks pretty on its own.

but:

The whole project, taken together, still looks like it was designed by one person.

That is the true value of architecture in the age of AI programming.


Open Source Code


🪐 Good luck 🪐