Skip to content

Response Models in Nexios

Response models define the structure and content of data your API returns. Nexios uses Pydantic models to provide automatic serialization, comprehensive OpenAPI documentation, and type-safe responses that ensure consistency across your API.

Why Use Response Models?

Response models provide essential benefits for API development:

  • Consistent Output: Ensures all responses follow the same structure and format
  • Type Safety: Full IDE support with autocompletion and type checking
  • Clear Documentation: API consumers know exactly what to expect from each endpoint
  • Automatic Serialization: Complex objects are automatically converted to JSON
  • Error Standardization: Consistent error response formats across your API
  • Validation: Ensures your API returns valid data structures

Basic Response Models

Define response schemas using Pydantic's BaseModel:

python
from nexios import NexiosApp
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime

class UserResponse(BaseModel):
    id: int = Field(..., description="Unique user identifier")
    username: str = Field(..., description="User's username")
    email: str = Field(..., description="User's email address")
    full_name: Optional[str] = Field(None, description="User's full name")
    is_active: bool = Field(..., description="Account activation status")
    created_at: datetime = Field(..., description="Account creation timestamp")
    last_login: Optional[datetime] = Field(None, description="Last login timestamp")

app = NexiosApp()

@app.get(
    "/users/{user_id}",
    responses={200: UserResponse},
    summary="Get user by ID",
    description="Retrieves detailed information for a specific user"
)
async def get_user(request, response, user_id: int):
    # Fetch user data
    user_data = {
        "id": user_id,
        "username": "johndoe",
        "email": "john@example.com",
        "full_name": "John Doe",
        "is_active": True,
        "created_at": datetime.now(),
        "last_login": datetime.now()
    }
    
    # Return validated response
    user = UserResponse(**user_data)
    return response.json(user.dict())

Multiple Response Models

Document different responses for various status codes and scenarios:

python
class UserResponse(BaseModel):
    id: int
    username: str
    email: str
    is_active: bool

class ErrorResponse(BaseModel):
    error: str = Field(..., description="Error message")
    code: int = Field(..., description="Error code")
    details: Optional[dict] = Field(None, description="Additional error details")
    timestamp: datetime = Field(default_factory=datetime.now, description="Error timestamp")

class ValidationErrorResponse(BaseModel):
    error: str = Field("Validation failed", description="Error type")
    code: int = Field(400, description="HTTP status code")
    field_errors: List[dict] = Field(..., description="Field-specific validation errors")

@app.get(
    "/users/{user_id}",
    responses={
        200: UserResponse,
        404: ErrorResponse,
        401: ErrorResponse,
        403: ErrorResponse,
        500: ErrorResponse
    },
    summary="Get user with comprehensive error handling"
)
async def get_user_with_errors(request, response, user_id: int):
    try:
        # Simulate different error conditions
        if user_id < 0:
            error = ErrorResponse(
                error="Invalid user ID",
                code=400,
                details={"user_id": user_id, "reason": "Must be positive"}
            )
            return response.json(error.dict(), status=400)
        
        if user_id == 999:
            error = ErrorResponse(
                error="User not found",
                code=404,
                details={"user_id": user_id}
            )
            return response.json(error.dict(), status=404)
        
        # Return successful response
        user = UserResponse(
            id=user_id,
            username="johndoe",
            email="john@example.com",
            is_active=True
        )
        return response.json(user.dict())
        
    except Exception as e:
        error = ErrorResponse(
            error="Internal server error",
            code=500,
            details={"exception": str(e)}
        )
        return response.json(error.dict(), status=500)

Collection Responses

Handle lists and paginated responses:

python
class UserListItem(BaseModel):
    id: int
    username: str
    email: str
    is_active: bool

class PaginatedUserResponse(BaseModel):
    users: List[UserListItem] = Field(..., description="List of users")
    total: int = Field(..., description="Total number of users")
    page: int = Field(..., description="Current page number")
    per_page: int = Field(..., description="Items per page")
    pages: int = Field(..., description="Total number of pages")
    has_next: bool = Field(..., description="Whether there are more pages")
    has_prev: bool = Field(..., description="Whether there are previous pages")

@app.get(
    "/users",
    responses={
        200: PaginatedUserResponse,
        400: ErrorResponse
    },
    summary="List users with pagination"
)
async def list_users(request, response):
    # Get pagination parameters
    page = int(request.query_params.get('page', 1))
    per_page = int(request.query_params.get('per_page', 20))
    
    # Simulate data fetching
    total_users = 150
    users_data = [
        {"id": i, "username": f"user{i}", "email": f"user{i}@example.com", "is_active": True}
        for i in range((page-1)*per_page + 1, min(page*per_page + 1, total_users + 1))
    ]
    
    # Create response
    response_data = PaginatedUserResponse(
        users=[UserListItem(**user) for user in users_data],
        total=total_users,
        page=page,
        per_page=per_page,
        pages=(total_users + per_page - 1) // per_page,
        has_next=page * per_page < total_users,
        has_prev=page > 1
    )
    
    return response.json(response_data.dict())

# Simple list response
@app.get(
    "/users/active",
    responses={200: List[UserListItem]},
    summary="Get all active users"
)
async def get_active_users(request, response):
    users = [
        UserListItem(id=1, username="alice", email="alice@example.com", is_active=True),
        UserListItem(id=2, username="bob", email="bob@example.com", is_active=True)
    ]
    return response.json([user.dict() for user in users])

Advanced Response Patterns

Nested Response Models

Handle complex, hierarchical data:

python
class AddressResponse(BaseModel):
    street: str
    city: str
    state: str
    zip_code: str
    country: str

class ProfileResponse(BaseModel):
    bio: Optional[str] = None
    avatar_url: Optional[str] = None
    website: Optional[str] = None
    social_links: dict = Field(default_factory=dict)

class DetailedUserResponse(BaseModel):
    id: int
    username: str
    email: str
    full_name: Optional[str]
    profile: ProfileResponse
    address: Optional[AddressResponse]
    created_at: datetime
    updated_at: datetime
    
    class Config:
        schema_extra = {
            "example": {
                "id": 123,
                "username": "johndoe",
                "email": "john@example.com",
                "full_name": "John Doe",
                "profile": {
                    "bio": "Software developer",
                    "avatar_url": "https://example.com/avatar.jpg",
                    "website": "https://johndoe.dev",
                    "social_links": {
                        "twitter": "@johndoe",
                        "github": "johndoe"
                    }
                },
                "address": {
                    "street": "123 Main St",
                    "city": "Anytown",
                    "state": "CA",
                    "zip_code": "12345",
                    "country": "US"
                },
                "created_at": "2024-01-01T12:00:00Z",
                "updated_at": "2024-01-15T10:30:00Z"
            }
        }

@app.get(
    "/users/{user_id}/detailed",
    responses={200: DetailedUserResponse, 404: ErrorResponse},
    summary="Get detailed user information"
)
async def get_detailed_user(request, response, user_id: int):
    # Fetch and return detailed user data
    pass

Generic Response Wrappers

Create consistent response formats:

python
from typing import TypeVar, Generic

T = TypeVar('T')

class ApiResponse(BaseModel, Generic[T]):
    success: bool = Field(..., description="Operation success status")
    message: str = Field(..., description="Response message")
    data: Optional[T] = Field(None, description="Response data")
    timestamp: datetime = Field(default_factory=datetime.now, description="Response timestamp")
    request_id: Optional[str] = Field(None, description="Request tracking ID")

class ApiErrorResponse(BaseModel):
    success: bool = Field(False, description="Operation success status")
    error: str = Field(..., description="Error message")
    code: int = Field(..., description="Error code")
    details: Optional[dict] = Field(None, description="Error details")
    timestamp: datetime = Field(default_factory=datetime.now, description="Error timestamp")
    request_id: Optional[str] = Field(None, description="Request tracking ID")

# Usage with generic wrapper
@app.get(
    "/users/{user_id}/wrapped",
    responses={
        200: ApiResponse[UserResponse],
        404: ApiErrorResponse,
        500: ApiErrorResponse
    }
)
async def get_user_wrapped(request, response, user_id: int):
    try:
        user = UserResponse(
            id=user_id,
            username="johndoe",
            email="john@example.com",
            is_active=True,
            created_at=datetime.now()
        )
        
        wrapped_response = ApiResponse[UserResponse](
            success=True,
            message="User retrieved successfully",
            data=user,
            request_id=request.headers.get('X-Request-ID')
        )
        
        return response.json(wrapped_response.dict())
        
    except Exception as e:
        error_response = ApiErrorResponse(
            error="Failed to retrieve user",
            code=500,
            details={"exception": str(e)},
            request_id=request.headers.get('X-Request-ID')
        )
        return response.json(error_response.dict(), status=500)

Response Customization

Custom Serialization

Handle special data types and formats:

python
from decimal import Decimal
from enum import Enum

class UserStatus(str, Enum):
    ACTIVE = "active"
    INACTIVE = "inactive"
    SUSPENDED = "suspended"
    PENDING = "pending"

class AccountResponse(BaseModel):
    id: int
    balance: Decimal = Field(..., description="Account balance")
    status: UserStatus = Field(..., description="Account status")
    created_at: datetime
    
    class Config:
        # Custom JSON encoders
        json_encoders = {
            Decimal: lambda v: float(v),
            datetime: lambda v: v.isoformat()
        }
        
        # Allow enum values in schema
        use_enum_values = True

@app.get(
    "/accounts/{account_id}",
    responses={200: AccountResponse},
    summary="Get account information"
)
async def get_account(request, response, account_id: int):
    account = AccountResponse(
        id=account_id,
        balance=Decimal('1234.56'),
        status=UserStatus.ACTIVE,
        created_at=datetime.now()
    )
    return response.json(account.dict())

Response Headers

Document custom response headers:

python
@app.get(
    "/users/{user_id}/download",
    responses={
        200: {
            "description": "User data export",
            "content": {
                "application/json": {
                    "schema": UserResponse.schema()
                }
            },
            "headers": {
                "X-Export-Format": {
                    "description": "Export format used",
                    "schema": {"type": "string"}
                },
                "X-Record-Count": {
                    "description": "Number of records exported",
                    "schema": {"type": "integer"}
                }
            }
        }
    }
)
async def export_user_data(request, response, user_id: int):
    user_data = UserResponse(
        id=user_id,
        username="johndoe",
        email="john@example.com",
        is_active=True,
        created_at=datetime.now()
    )
    
    # Set custom headers
    response.headers['X-Export-Format'] = 'json'
    response.headers['X-Record-Count'] = '1'
    
    return response.json(user_data.dict())

Best Practices

Model Organization

python
# models/responses.py
class BaseResponse(BaseModel):
    """Base response with common fields"""
    timestamp: datetime = Field(default_factory=datetime.now)
    request_id: Optional[str] = None

class UserBaseResponse(BaseResponse):
    """Base user response fields"""
    id: int
    username: str
    email: str

class UserSummaryResponse(UserBaseResponse):
    """Minimal user information"""
    is_active: bool

class UserDetailResponse(UserBaseResponse):
    """Complete user information"""
    full_name: Optional[str]
    profile: ProfileResponse
    created_at: datetime
    updated_at: datetime

Error Response Consistency

python
class StandardErrorResponse(BaseModel):
    error: str
    code: int
    message: str
    details: Optional[dict] = None
    timestamp: datetime = Field(default_factory=datetime.now)
    
    @classmethod
    def not_found(cls, resource: str, id: Any):
        return cls(
            error="NOT_FOUND",
            code=404,
            message=f"{resource} with id {id} not found",
            details={"resource": resource, "id": str(id)}
        )
    
    @classmethod
    def validation_error(cls, errors: List[dict]):
        return cls(
            error="VALIDATION_ERROR",
            code=400,
            message="Request validation failed",
            details={"field_errors": errors}
        )

Testing Response Models

python
def test_user_response_serialization():
    user_data = {
        "id": 123,
        "username": "testuser",
        "email": "test@example.com",
        "is_active": True,
        "created_at": datetime.now()
    }
    
    user_response = UserResponse(**user_data)
    json_data = user_response.dict()
    
    assert json_data["id"] == 123
    assert json_data["username"] == "testuser"
    assert "created_at" in json_data

def test_error_response_factory():
    error = StandardErrorResponse.not_found("User", 123)
    assert error.code == 404
    assert "User with id 123 not found" in error.message

Response models are crucial for creating consistent, well-documented APIs. They ensure that your API returns predictable data structures and provide clear contracts for API consumers, making integration easier and more reliable.