Skip to main content

Responses

Framefox controllers provide several methods to return different types of HTTP responses. All response types are built on top of FastAPI’s response classes.

HTMLResponse with render()

Render HTML templates and return them as HTTP responses.

Basic Usage

from framefox.core.controller.abstract_controller import AbstractController
from framefox.core.routing.decorator.route import Route

class PageController(AbstractController):
    @Route(path="/about", name="about", methods=["GET"])
    async def about(self):
        return self.render("about.html")

Passing Context Data

Pass data to templates using the context parameter:
@Route(path="/users/{user_id}", name="user_profile", methods=["GET"])
async def show_profile(self, user_id: int):
    user = {
        "id": user_id,
        "name": "John Doe",
        "email": "[email protected]",
        "joined": "2024-01-15"
    }
    
    return self.render("user/profile.html", {
        "user": user,
        "page_title": "User Profile"
    })

Template Path Resolution

Template paths are relative to your configured templates directory:
# Renders: templates/products/list.html
return self.render("products/list.html", {"products": products})

# Renders: templates/admin/dashboard.html
return self.render("admin/dashboard.html", {"stats": stats})

Method Signature

template_path
str
required
Path to the template file to render, relative to the templates directory
context
dict
default:"None"
Dictionary of variables to pass to the template. Available in the template as {{ variable_name }}
Returns: HTMLResponse with status code 200 Raises: Exception if the template file is not found or contains errors

JSONResponse with json()

Return data as JSON, typically for API endpoints.

Basic Usage

class ApiController(AbstractController):
    @Route(path="/api/status", name="api_status", methods=["GET"])
    async def status(self):
        return self.json({
            "status": "ok",
            "version": "1.0.0",
            "timestamp": "2024-01-15T10:30:00Z"
        })

Custom Status Codes

Set custom HTTP status codes for different scenarios:
@Route(path="/api/users", name="api_user_create", methods=["POST"])
async def create_user(self, request: Request):
    data = await request.json()
    
    # Create user logic here
    user = {
        "id": 123,
        "name": data["name"],
        "email": data["email"]
    }
    
    # Return 201 Created
    return self.json(user, status=201)

Error Responses

Return error messages with appropriate status codes:
@Route(path="/api/users/{user_id}", name="api_user_detail", methods=["GET"])
async def get_user(self, user_id: int, user_service: UserService):
    user = user_service.find(user_id)
    
    if not user:
        return self.json(
            {"error": "User not found", "user_id": user_id},
            status=404
        )
    
    return self.json({
        "id": user.id,
        "name": user.name,
        "email": user.email
    })

Common Status Codes

  • 200 OK - Successful GET request
  • 201 Created - Successful POST request that created a resource
  • 204 No Content - Successful request with no response body
  • 400 Bad Request - Invalid request data
  • 401 Unauthorized - Authentication required
  • 403 Forbidden - Insufficient permissions
  • 404 Not Found - Resource doesn’t exist
  • 422 Unprocessable Entity - Validation failed
  • 500 Internal Server Error - Server error

Method Signature

data
dict
required
The data to serialize as JSON. Must be JSON-serializable (dict, list, str, int, float, bool, None)
status
int
default:"200"
HTTP status code for the response
Returns: JSONResponse with the specified status code

Nested Data Structures

@Route(path="/api/dashboard", name="api_dashboard", methods=["GET"])
async def dashboard(self):
    return self.json({
        "user": {
            "id": 1,
            "name": "John Doe",
            "roles": ["admin", "editor"]
        },
        "stats": {
            "posts": 42,
            "comments": 158,
            "likes": 523
        },
        "recent_activity": [
            {"type": "post", "title": "New article", "date": "2024-01-15"},
            {"type": "comment", "content": "Great post!", "date": "2024-01-14"}
        ]
    })

RedirectResponse with redirect()

Redirect users to different URLs.

Basic Usage

class AuthController(AbstractController):
    @Route(path="/logout", name="logout", methods=["POST"])
    async def logout(self):
        # Clear session/cookies
        return self.redirect("/login")

Redirect After Form Submission

@Route(path="/posts/create", name="post_create", methods=["GET", "POST"])
async def create_post(self, request: Request):
    if request.method == "GET":
        return self.render("posts/create.html")
    
    # Handle POST - create the post
    # ... save post logic ...
    
    self.flash("success", "Post created successfully!")
    return self.redirect("/posts")

Redirect with Different Status Codes

# Temporary redirect (302) - default
@Route(path="/temp-page", name="temp_page", methods=["GET"])
async def temp_page(self):
    return self.redirect("/new-page")  # 302 Found

# Permanent redirect (301)
@Route(path="/old-url", name="old_url", methods=["GET"])
async def old_url(self):
    return self.redirect("/new-url", code=301)  # 301 Moved Permanently

# See Other (303) - after POST request
@Route(path="/form-submit", name="form_submit", methods=["POST"])
async def form_submit(self):
    # Process form
    return self.redirect("/success", code=303)  # 303 See Other

# Temporary redirect (307) - preserves request method
@Route(path="/api-v1/users", name="api_v1_users", methods=["POST"])
async def api_v1_users(self):
    return self.redirect("/api-v2/users", code=307)  # 307 Temporary Redirect

Redirect to Named Routes

Combine generate_url() with redirect() for type-safe redirects:
@Route(path="/users/{user_id}/delete", name="user_delete", methods=["POST"])
async def delete_user(self, user_id: int):
    # Delete user logic
    
    self.flash("success", f"User {user_id} deleted")
    
    # Redirect to the user list using route name
    return self.redirect(self.generate_url("user_list"))

Redirect with Query Parameters

@Route(path="/search", name="search", methods=["POST"])
async def search_form(self, request: Request):
    form_data = await request.form()
    query = form_data.get("query")
    
    # Redirect to GET endpoint with query parameter
    return self.redirect(f"/search/results?q={query}")

Method Signature

location
str
required
The URL to redirect to. Can be a relative path (e.g., /users) or absolute URL (e.g., https://example.com)
code
int
default:"302"
HTTP status code for the redirect. Common values:
  • 301 - Moved Permanently
  • 302 - Found (temporary redirect)
  • 303 - See Other (typically after POST)
  • 307 - Temporary Redirect (preserves method)
  • 308 - Permanent Redirect (preserves method)
Returns: RedirectResponse with the specified status code

Custom Responses

For advanced use cases, you can return custom FastAPI responses directly:

Plain Text Response

from fastapi.responses import PlainTextResponse

@Route(path="/robots.txt", name="robots", methods=["GET"])
async def robots(self):
    content = """
    User-agent: *
    Disallow: /admin/
    Allow: /
    """
    return PlainTextResponse(content=content)

File Response

from fastapi.responses import FileResponse

@Route(path="/download/{file_id}", name="download_file", methods=["GET"])
async def download(self, file_id: int):
    file_path = f"/var/files/{file_id}.pdf"
    return FileResponse(
        path=file_path,
        filename="document.pdf",
        media_type="application/pdf"
    )

Streaming Response

from fastapi.responses import StreamingResponse
import io

@Route(path="/export/csv", name="export_csv", methods=["GET"])
async def export_csv(self):
    def generate_csv():
        yield "Name,Email,Age\n"
        yield "John Doe,[email protected],30\n"
        yield "Jane Smith,[email protected],25\n"
    
    return StreamingResponse(
        generate_csv(),
        media_type="text/csv",
        headers={"Content-Disposition": "attachment; filename=users.csv"}
    )

Custom Headers

from fastapi.responses import Response

@Route(path="/api/data", name="api_data", methods=["GET"])
async def api_data(self):
    response = self.json({"data": "value"})
    response.headers["X-Custom-Header"] = "CustomValue"
    response.headers["Cache-Control"] = "max-age=3600"
    return response

Response Patterns

API Response Wrapper

Create consistent API responses:
class ApiController(AbstractController):
    def api_response(self, data=None, message=None, status=200):
        return self.json({
            "success": 200 <= status < 300,
            "message": message,
            "data": data
        }, status=status)
    
    @Route(path="/api/users/{user_id}", name="api_user_get", methods=["GET"])
    async def get_user(self, user_id: int):
        user = {"id": user_id, "name": "John"}
        return self.api_response(
            data=user,
            message="User retrieved successfully"
        )

Conditional Responses

Return different response types based on request headers:
@Route(path="/users/{user_id}", name="user_detail", methods=["GET"])
async def user_detail(self, user_id: int, request: Request):
    user = {"id": user_id, "name": "John Doe"}
    
    # Check if client wants JSON
    accept = request.headers.get("Accept", "")
    if "application/json" in accept:
        return self.json(user)
    
    # Default to HTML
    return self.render("user/detail.html", {"user": user})

Best Practices

Always return the correct HTTP status code. Use 201 for created resources, 404 for not found, etc.
Follow the Post-Redirect-Get (PRG) pattern to prevent duplicate form submissions.
When redirecting after an action, add a flash message to inform the user of the result.
Use Pydantic models to validate JSON request bodies before processing.
Return appropriate error responses with helpful messages instead of letting exceptions propagate.

Next Steps

Forms

Learn about form handling and validation

Controllers Overview

Back to controller basics

Build docs developers (and LLMs) love