AI is very good at quickly writing API endpoints. For example, if you tell it:

“Add an endpoint to create a user.”

It can probably produce working code in a few minutes. But without unified standards in the project, different endpoints will quickly end up like this:

{
  "success": true
}

Another endpoint returns:

{
  "code": 0,
  "msg": "ok",
  "result": {}
}

When yet another endpoint encounters an error, it directly returns:

{
  "error": "user not found"
}

Some places even throw raw database exceptions back to the frontend.

Every single endpoint “works,” but the entire system becomes increasingly hard to maintain.

So, there’s a very important but often overlooked category of foundational architecture in AI programming:

Unified response values + global exception handling + unified parameter validation.

Their purpose is to define upfront:

How to return on success, how to return on failure, and where to intercept invalid input.


📌 Technical Profile

Unified Exception and API Encapsulation Architecture

Through a unified response model, unified exception hierarchy, and unified input validation mechanism, all API endpoints in the system share a consistent approach to input, output, and error handling.

Here are three common core concepts:

  • API Response Envelope: The API response wrapper, i.e., defining the structure all endpoints return uniformly;
  • Global Exception Handling: Centralized exception handling, rather than each endpoint writing its own;
  • Validator: A validator used to check format, range, and validity before data enters business logic.

💡 One-Sentence Understanding

Think of an API as airport security.

After a passenger enters the airport:

Check ID
Check luggage
Passes all rules
Enter boarding area

If there’s a problem:

ID error
Luggage violation
Identity issue

Each gate doesn’t decide on its own:

“What should we do with this person?”

Instead, a unified security process handles it.

Software systems are the same:

User request
Parameter validation
Business processing
Unified response

When an exception occurs:

Business exception
System exception
Global exception handler
Standard error response

This is especially important for AI. Because without rules, AI easily:

Reinvents a new response format and error handling approach for every endpoint it writes.


I. Negative Example: AI’s “Free-Style”

Suppose we ask AI to implement a create user endpoint:

Username must be at least 3 characters, password must meet security rules. If the username already exists, provide an error message.

Without architectural constraints, AI might write:

@router.post("/users")
async def create_user(data: dict):
    if len(data["username"]) < 3:
        return {
            "success": False,
            "message": "username too short"
        }

    if len(data["password"]) < 8:
        return {
            "code": 400,
            "error": "invalid password"
        }

    user = await db.get_user(data["username"])

    if user:
        raise HTTPException(
            status_code=400,
            detail="user already exists"
        )

    try:
        new_user = await db.create_user(data)

        return {
            "result": new_user,
            "status": "ok"
        }

    except Exception as e:
        return {
            "error": str(e)
        }

The functionality seems complete.

But many problems have already emerged inside.


Problem 1: Inconsistent Response Formats

Within the same endpoint, there’s even:

{
  "success": false
}

And:

{
  "code": 400
}

As well as:

{
  "status": "ok"
}

Every time the frontend calls an endpoint, it has to guess again:

Should I check success, code, or status this time?


Problem 2: Parameter Validation Scattered in Business Code

if len(data["username"]) < 3:
if len(data["password"]) < 8:

These essentially belong to:

Is the input valid?

Yet they’re mixed into business logic.

The next AI writing a “modify user” endpoint will likely copy the same thing again.


Problem 3: Inconsistent Exception Handling

Username exists:

raise HTTPException(...)

Password error:

return {...}

Database exception:

except Exception as e:
    return {"error": str(e)}

Three types of errors, three different handling approaches.


Problem 4: Internal System Errors Directly Exposed to Users

str(e)

Might return:

Database table names
SQL statements
Server paths
Internal configuration

Directly to the frontend.

This isn’t just ugly — it can also introduce security risks.


Problem 5: Every Endpoint Repeats from Scratch

Once the system has 100 endpoints, you might see:

100 sets of parameter checks
100 sets of try / except
20 response structures
10 error formats

The real problem isn’t that AI can’t write code.

It’s that:

Without unified standards, AI will very efficiently produce inconsistency.


II. Architecture Rules: Humans Draw the “Blueprint” First

To solve this problem, the entire request flow can be standardized:

Client request
Schema / Validator
Input validation
API
Service
Business exception
Global Exception Handler
Global exception handling
ApiResponse
Unified response

Then give AI a few clear rules.


Rule 1: All Standard APIs Use a Unified Response Structure

For example, unify on:

{
  "code": 200,
  "message": "success",
  "data": {}
}

The three fields each have their own role:

code
Business / status code

message
Human-readable status message

data
The actual returned data

On success:

{
  "code": 200,
  "message": "success",
  "data": {
    "id": 12,
    "username": "tom"
  }
}

On failure:

{
  "code": 404,
  "message": "User '12' not found"
}

The frontend doesn’t have to guess anymore.


Rule 2: APIs Should Not Build JSON Ad Hoc Everywhere

Don’t:

return {
    "success": True,
    "result": data
}

Don’t either:

return {
    "status": "ok",
    "payload": data
}

Use uniformly:

return ApiResponse(data=data)

This way the response format has only one definition.


Rule 3: Input Format Is Handled by Schema and Validator

For example:

class UserCreate(BaseModel):
    username: str = Field(
        ...,
        min_length=3,
        max_length=100
    )

Here, Field can be understood as:

Field rules.

It directly specifies:

username
Minimum 3 characters
Maximum 100 characters

AI doesn’t need to rewrite in every API:

if len(username) < 3:

Rule 4: Complex Fields Use Validators

Some validations can’t be done with simple length checks.

For example, a password might require:

Minimum number of characters
Contains uppercase and lowercase letters
Contains digits
Contains special characters

This is when it’s appropriate to use:

Validator

Concentrating the password rules in one place.

Later, for:

Create user
Reset password
Change password

All reuse the same rules.


Rule 5: Business Errors Use Unified Exception Types

For example:

User not found
Agent not found
Knowledge base not found

They all belong to:

Not Found (resource doesn’t exist)

Can uniformly use:

NotFoundError

And:

Username already exists
Agent name is duplicated

Can uniformly be classified as:

AlreadyExistsError

This way the Service only needs to express:

“What business error occurred.”

Without worrying about whether HTTP should return 404, 409, or another status code.


Rule 6: Exceptions Are Handled Centrally Globally

Business code can:

raise NotFoundError(...)

The global exception handler is responsible for:

NotFoundError
HTTP 404
ApiResponse

Instead of every endpoint:

try:
    ...
except:
    ...

Repeated dozens of times.


III. What Are the Benefits of Doing This?

For traditional development, this is called engineering standards. For AI programming, it has even more direct value.


1. AI No Longer Arbitrarily Creates Response Structures

The project only has:

ApiResponse

This one set of rules.

After AI sees existing code, it’s more likely to continue writing:

return ApiResponse(data=result)

2. Frontend Calls Become Very Simple

The frontend can uniformly assume:

code
message
data

Always exist.

So the unified HTTP Client can handle:

Success
Error
Token expired
Notification messages

Without each page needing to adapt individually.


3. Validation Rules Don’t Scatter

For example, if the password rule changes:

Minimum 8 characters becomes minimum 12.

If the rule is concentrated in a Validator, change it in one place.

If it’s scattered across:

Registration
Create user
Change password
Reset password
Admin backend

Five endpoints, it’s easy to miss one.


4. Service Becomes Cleaner

Business code doesn’t need to repeatedly:

try:
    ...
except HTTPException:
    ...

It only handles business:

User not found
Raise NotFoundError

User already exists
Raise AlreadyExistsError

How errors convert to HTTP is handled uniformly by the outer layer.


5. AI Can More Easily Understand “Which Error Belongs Where”

Very clear boundaries can be established:

Input format error
 → Validator

Business rule error
 → Domain Error

Unknown system error
 → Global Exception Handler

Response format
 → ApiResponse

This is essentially layering error handling as well.


IV. Prompt Implementation: Teach the Rules to AI

Just telling AI:

“Pay attention to exception handling.”

Has almost no practical effect.

What’s more effective is to write it as concrete engineering rules.

For example, add to project-level rules:

## API Response and Validation Rules

All standard JSON APIs must use the project's unified
ApiResponse response envelope.

Standard response fields:
- code
- message
- data

Rules:
- Do not create alternative response structures such as success/result/payload/status unless explicitly required by an existing protocol.
- API routes should return ApiResponse, not manually construct response dictionaries.
- Request payloads must use existing Pydantic schemas.
- Simple input constraints (e.g., length, range, and required fields) should be declared in Pydantic field definitions.
- Reusable or complex validation rules should use the project's existing validators.
- Do not duplicate validation logic in API routes.
- Business errors must use the project's domain exception types, e.g., NotFoundError and AlreadyExistsError.
- Do not convert business errors to HTTPException in services.
- Do not add repetitive try/except blocks to every API route.
- Let the global exception handling mechanism convert known exceptions into standardized API responses.
- Unexpected internal exceptions must not expose sensitive implementation details in production.

Later, when asking AI to develop an endpoint, you can further prompt:

Implement the "create user" endpoint.

Please strictly follow the project's existing unified API standards:

1. Request parameters use existing Pydantic Schema;
2. Field length, range, and similar rules go in Schema / Validator;
3. API responses uniformly use ApiResponse;
4. Business errors like "user already exists" use existing Domain Errors;
5. Do not repeat try/except in the Router;
6. Do not create new error response formats on your own;
7. Prioritize reusing the project's existing Validators and exception types.

Before implementing, first check:
app/schemas/common.py
Related business Schemas
Existing Services
Global exception handling code.

Now AI is no longer “freely designing an endpoint,” but rather:

Adding an endpoint under the existing exception and response protocol.


V. Positive Output: How Does miniagent Do It?

miniagent has already combined:

Unified response
Business exceptions
Global exception handling
Pydantic parameter validation

into one cohesive system, as shown in the diagram below:

flowchart TD EX["Python Exception
Python Exception Base Class"] BASE["BaseDomainError
Business Exception Base Class"] NOTFOUND["NotFoundError
Resource Not Found"] EXISTS["AlreadyExistsError
Resource Already Exists"] EMPTY["EmptyDataError
Data Is Empty"] BAD["BadRequestError
Bad Request"] READONLY["ReadOnlyError
Resource Is Read-Only"] INVALID["InvalidValueError
Invalid Value"] HANDLER["Global Exception Handler
Global Exception Handling"] RESPONSE["ApiResponse
Unified Response Format
code · message · data"] EX --> BASE BASE --> NOTFOUND BASE --> EXISTS BASE --> EMPTY BASE --> BAD BASE --> READONLY BASE --> INVALID NOTFOUND --> HANDLER EXISTS --> HANDLER EMPTY --> HANDLER BAD --> HANDLER READONLY --> HANDLER INVALID --> HANDLER HANDLER --> RESPONSE

1. Unified Response Value: ApiResponse

miniagent defines the unified top-level response model in backend/app/schemas/common.py:

class ApiResponse(BaseModel, Generic[T]):
    """
    Generic top-level API response envelope.
    """

    code: int = Field(
        200,
        description="Business status code, 200 = success"
    )

    message: str = Field(
        "success",
        description="Human-readable status message"
    )

    data: Optional[T] = Field(
        None,
        description="Response payload"
    )

That is, standard endpoints uniformly revolve around:

{
  "code": 200,
  "message": "success",
  "data": {}
}

It also uses:

Generic[T]

Generic means generics.

It can be simply understood as:

data can hold different types of data, but the outer code / message / data shell remains unchanged.

For example:

ApiResponse[UserOut]
ApiResponse[AgentOut]
ApiResponse[PageResult]

Different inner data, unified outer protocol.


2. Paginated Results Are Also Uniformly Encapsulated

The same file also defines:

class PageResult(BaseModel, Generic[T]):
    total: int
    page: int
    page_size: int
    data: List[T]

So paginated endpoints don’t return:

rows
count
current

today and then:

items
total
pageNum

tomorrow.

Instead, they uniformly use:

{
  "total": 100,
  "page": 1,
  "page_size": 20,
  "data": []
}

This is especially important for AI, because when adding a paginated endpoint, it can directly reuse the existing structure.


3. Business Exceptions Also Have a Unified Base Class

miniagent defines:

class BaseDomainError(Exception):
    """
    Business Logic Exception Base Class
    """

That is:

The base class for domain business exceptions.

Then continues to derive:

class NotFoundError(BaseDomainError):
    ...

class AlreadyExistsError(BaseDomainError):
    ...

class EmptyDataError(BaseDomainError):
    ...

class ReadOnlyError(BaseDomainError):
    ...

class InvalidValueError(BaseDomainError):
    ...

This way, when business code encounters errors, it doesn’t need to create:

UserNotExistException
MissingAgentException
KBNotFoundException
NoDocumentException

Various entirely different error hierarchies. Instead, it tries to fit into existing semantics.


4. Error Messages Are Also Unified with I18n

Here, I18n means:

Internationalization.

BaseDomainError can use:

def to_detail(self) -> str:
    return _translate(...)

To convert exceptions into messages in the corresponding language.

This means:

Business exception
Unified error type
Unified internationalized message

Instead of each AI hardcoding a new message when writing an endpoint:

"User not found"

VII. Global Exceptions: Only Need to “Raise,” Not “Catch” Everywhere

miniagent provides in the application entry point:

def handle_exception(exc: Exception) -> JSONResponse:

Which uniformly handles different exception types, for example:

Service
NotFoundError
Global Exception Handler
HTTP 404
ApiResponse

For:

AlreadyExistsError

It uniformly converts to:

HTTP 409 Conflict

Where Conflict means:

Resource state conflict.

For example, creating a username that already exists fits this semantics well.


Unknown Exceptions Also Have a Unified Fallback

If it’s not a known business exception:

error_data = {
    "error":
        str(exc)
        if settings.debug
        else t("common.error_500")
}

This reflects a very important production environment principle:

Development environment:

Can see detailed errors
Convenient for debugging

Production environment:

Hide internal implementation
Return unified error message

Avoiding directly exposing internal exceptions to regular users.


VI. Validator: Invalid Input Should Not Enter the Business Layer

Let’s look at miniagent’s user Schema.

It doesn’t write inside the create user API:

if len(username) < 3:

Instead, it directly defines:

class UserCreate(BaseModel):

    username: str = Field(
        ...,
        min_length=3,
        max_length=100
    )

    nickname: Optional[str] = Field(
        None,
        max_length=100
    )

    avatar: Optional[str] = Field(
        None,
        max_length=500
    )

This means:

When the username length is invalid, the request is intercepted by the data model before it even enters the core business logic.


Password Validation Further Reuses the Validator

miniagent currently defines:

PasswordValue = Annotated[
    str,
    Field(max_length=128),
    AfterValidator(validate_password)
]

Two terms appear here.

Annotated

Annotated can be understood as:

Attaching additional rules to a data type.

Here the base type is still:

str

But with the addition of:

Maximum length 128
+
Password Validator

AfterValidator

AfterValidator can be understood as:

After base type validation completes, execute a custom validation function.

Here it calls:

validate_password

So creating a user:

class UserCreate(BaseModel):
    password: PasswordValue

Resetting a password:

class UserPasswordReset(BaseModel):
    password: PasswordValue

Both reuse the same password rules.

This is a textbook example of:

Define once, reuse everywhere.


VII. Ultimately Forming a Very Clear Request Chain

Combining miniagent’s actual implementations, we get:

miniagent’s request chain

Now different responsibilities become very clear:

Schema / Validator
Responsible for "is the input valid"

Service
Responsible for "can the business do this"

Domain Error
Responsible for "what business problem occurred"

Global Exception Handler
Responsible for "how errors convert to HTTP responses"

ApiResponse
Responsible for "what the final return looks like"

This is the true value of a unified exception architecture.


VIII. How Will AI Write After Architecture Empowerment?

Suppose we now tell AI:

“Add a feature to modify the username.”

Without architecture, AI might design from scratch:

Parameter checks
Return JSON
try / except
Error messages

With rules like miniagent’s, it should first think:

1. Does UserUpdate already have a username field?
2. Does Field already specify length?
3. Add business operation in UserService
4. Raise AlreadyExistsError on duplicate name
5. Router calls Service
6. Return ApiResponse

Rather than reinventing a set of rules.

This is what we call:

AI output after architecture empowerment.

It’s not that AI suddenly got “smarter,” but rather:

We reduced the things it can freely decide.


Conclusion

Unified exception and API encapsulation may look like just a few unassuming base classes:

ApiResponse
PageResult
BaseDomainError
Validator

But for AI programming, they actually establish a very important set of “traffic rules.”

It tells AI:

How to validate input
How to return on success
How to express business errors
Where system errors are handled
What the frontend ultimately sees

Without these rules, AI will freely improvise in every endpoint. And the better AI gets at writing code, the faster this “inconsistency” accumulates.

Therefore:

Good exception architecture isn’t about adding a few base classes to the code. It’s about ensuring the entire system has only one error language and one API language.

In the era of AI programming, these standards should be固化 into Project Rules, System Prompts, or project context upfront.

Ultimately forming:

Invalid input
 → Intercepted by Validator

Business disallowed
 → Expressed as Domain Error

System exception
 → Caught by Global Handler fallback

Whether success or failure
 → Returned via unified protocol

This way, every new endpoint AI adds is actually reusing the same engineering rules.

Don’t let AI reinvent “what success means and what failure means” for every endpoint it writes.

First unify the rules, then let AI write business. This is the true value of unified exception and encapsulation architecture in AI programming.

Open Source Code


🪐 Good luck 🪐