Skip to main content

Overview

FastrAPI provides multiple response types for returning different content formats. All response classes are implemented in Rust for maximum performance.

JSONResponse

The default response type for returning JSON data:
from fastrapi import FastrAPI
from fastrapi.responses import JSONResponse

app = FastrAPI()

@app.get("/users")
def get_users() -> JSONResponse:
    return JSONResponse({
        "users": [
            {"id": 1, "name": "Alice"},
            {"id": 2, "name": "Bob"}
        ]
    })

# Equivalent shorthand (JSONResponse is default)
@app.get("/items")
def get_items():
    return {"items": ["item1", "item2"]}

Constructor

JSONResponse(content, status_code=200)
Parameters:
  • content: Any JSON-serializable Python object (dict, list, etc.)
  • status_code: HTTP status code (default: 200)

Implementation

JSONResponse is a Rust struct that serializes Python objects to JSON:
// src/responses.rs
#[pyclass(name = "JSONResponse")]
#[derive(Clone)]
pub struct PyJSONResponse {
    #[pyo3(get)]
    pub content: Py<PyAny>,
    #[pyo3(get)]
    pub status_code: u16,
}

#[pymethods]
impl PyJSONResponse {
    #[new]
    #[pyo3(signature = (content, status_code=200))]
    fn new(content: Py<PyAny>, status_code: u16) -> Self {
        Self { content, status_code }
    }
}
The content is converted to JSON using Rust’s serde_json:
// src/py_handlers.rs
fn convert_json_response(py: Python, result: &Bound<PyAny>) -> Response {
    if let Ok(resp) = result.extract::<PyRef<'_, PyJSONResponse>>() {
        let status_code = StatusCode::from_u16(resp.status_code)
            .unwrap_or(StatusCode::OK);
        let json = py_any_to_json(py, &resp.content.bind(py));
        (status_code, Json(json)).into_response()
    }
}

Status codes

Customize the HTTP status code:
from fastrapi.responses import JSONResponse

@app.post("/users")
def create_user(user):
    # 201 Created
    return JSONResponse(
        {"id": 1, "name": user.name},
        status_code=201
    )

@app.get("/not-found")
def not_found():
    # 404 Not Found
    return JSONResponse(
        {"error": "Resource not found"},
        status_code=404
    )

HTMLResponse

Return HTML content:
from fastrapi.responses import HTMLResponse

@app.get("/hello")
def hello_html() -> HTMLResponse:
    return HTMLResponse("""
        <!DOCTYPE html>
        <html>
            <head><title>FastrAPI</title></head>
            <body>
                <h1>Hello from FastrAPI!</h1>
                <p>This is HTML content.</p>
            </body>
        </html>
    """)

Constructor

HTMLResponse(content, status_code=200)
Parameters:
  • content: HTML string
  • status_code: HTTP status code (default: 200)

Implementation

#[pyclass(name = "HTMLResponse")]
#[derive(Clone)]
pub struct PyHTMLResponse {
    #[pyo3(get)]
    pub content: String,
    #[pyo3(get)]
    pub status_code: u16,
}
HTMLResponse sets the Content-Type header to text/html; charset=utf-8:
fn convert_html_response(_py: Python, result: &Bound<PyAny>) -> Response {
    if let Ok(resp) = result.extract::<PyRef<'_, PyHTMLResponse>>() {
        let status_code = StatusCode::from_u16(resp.status_code)
            .unwrap_or(StatusCode::OK);
        (status_code, Html(resp.content.clone())).into_response()
    }
}

Dynamic HTML

Generate HTML dynamically:
@app.get("/user/{name}")
def user_page(name: str) -> HTMLResponse:
    html = f"""
        <!DOCTYPE html>
        <html>
            <head><title>{name}'s Profile</title></head>
            <body>
                <h1>Welcome, {name}!</h1>
                <p>This is your profile page.</p>
            </body>
        </html>
    """
    return HTMLResponse(html)

PlainTextResponse

Return plain text content:
from fastrapi.responses import PlainTextResponse

@app.get("/text")
def plain_text() -> PlainTextResponse:
    return PlainTextResponse("This is plain text content.")

@app.get("/log")
def get_log() -> PlainTextResponse:
    return PlainTextResponse(
        "2024-01-01 12:00:00 INFO Server started\n"
        "2024-01-01 12:00:01 INFO Request received\n"
        "2024-01-01 12:00:02 INFO Response sent"
    )

Constructor

PlainTextResponse(content, status_code=200)
Parameters:
  • content: Plain text string
  • status_code: HTTP status code (default: 200)

Implementation

#[pyclass(name = "PlainTextResponse")]
#[derive(Clone)]
pub struct PyPlainTextResponse {
    #[pyo3(get)]
    pub content: String,
    #[pyo3(get)]
    pub status_code: u16,
}
Sets Content-Type to text/plain; charset=utf-8:
fn convert_text_response(_py: Python, result: &Bound<PyAny>) -> Response {
    if let Ok(resp) = result.extract::<PyRef<'_, PyPlainTextResponse>>() {
        let status_code = StatusCode::from_u16(resp.status_code)
            .unwrap_or(StatusCode::OK);
        (
            status_code,
            [(header::CONTENT_TYPE, "text/plain; charset=utf-8")],
            resp.content.clone(),
        )
            .into_response()
    }
}

RedirectResponse

Redirect to another URL:
from fastrapi.responses import RedirectResponse

@app.get("/old-path")
def old_endpoint() -> RedirectResponse:
    # 307 Temporary Redirect (default)
    return RedirectResponse("/new-path")

@app.get("/moved")
def permanent_redirect() -> RedirectResponse:
    # 301 Permanent Redirect
    return RedirectResponse(
        "https://example.com/new-location",
        status_code=301
    )

Constructor

RedirectResponse(url, status_code=307)
Parameters:
  • url: Target URL (can be relative or absolute)
  • status_code: HTTP redirect status code (default: 307)

Common redirect status codes

CodeDescriptionUse case
301Permanent RedirectResource moved permanently
302FoundTemporary redirect (old)
303See OtherRedirect after POST
307Temporary RedirectTemporary redirect (default)
308Permanent RedirectPermanent redirect with method

Implementation

#[pyclass(name = "RedirectResponse")]
#[derive(Clone)]
pub struct PyRedirectResponse {
    #[pyo3(get)]
    pub url: String,
    #[pyo3(get)]
    pub status_code: u16,
}
Redirects use Axum’s Redirect helper:
fn convert_redirect_response(_py: Python, result: &Bound<PyAny>) -> Response {
    if let Ok(resp) = result.extract::<PyRef<'_, PyRedirectResponse>>() {
        if resp.status_code == 301 {
            Redirect::permanent(&resp.url).into_response()
        } else {
            Redirect::temporary(&resp.url).into_response()
        }
    }
}

Auto response detection

If you don’t specify a response type, FastrAPI automatically converts your return value:
@app.get("/auto")
def auto_response():
    # Automatically converted to JSONResponse
    return {"message": "Auto-detected"}

@app.get("/null")
def null_response():
    # Returns 204 No Content
    return None
The auto-detection logic:
fn convert_auto_response(py: Python, result: &Bound<PyAny>) -> Response {
    if result.is_none() {
        return StatusCode::NO_CONTENT.into_response();
    }
    
    // Convert to JSON
    let json = py_any_to_json(py, result);
    (StatusCode::OK, Json(json)).into_response()
}

Response type annotation

Use type hints to specify the response type:
from fastrapi.responses import HTMLResponse, JSONResponse, PlainTextResponse

@app.get("/html")
def html_page() -> HTMLResponse:
    return HTMLResponse("<h1>Hello</h1>")

@app.get("/json")
def json_data() -> JSONResponse:
    return JSONResponse({"data": "value"})

@app.get("/text")
def text_data() -> PlainTextResponse:
    return PlainTextResponse("Hello World")
FastrAPI parses return type annotations at decorator time to optimize response handling:
// src/pydantic.rs
let response_type = if let Ok(return_annotation) = func.getattr("__annotations__") {
    // Check return type annotation
    if annotation_str.contains("HTMLResponse") {
        ResponseType::Html
    } else if annotation_str.contains("JSONResponse") {
        ResponseType::Json
    } else if annotation_str.contains("PlainTextResponse") {
        ResponseType::PlainText
    } else if annotation_str.contains("RedirectResponse") {
        ResponseType::Redirect
    } else {
        ResponseType::Auto
    }
} else {
    ResponseType::Auto
};

Complete example

from fastrapi import FastrAPI
from fastrapi.responses import (
    HTMLResponse,
    JSONResponse,
    PlainTextResponse,
    RedirectResponse
)

app = FastrAPI()

@app.get("/")
def root() -> HTMLResponse:
    return HTMLResponse("""
        <html>
            <body>
                <h1>Welcome to FastrAPI</h1>
                <ul>
                    <li><a href="/api/data">JSON API</a></li>
                    <li><a href="/text">Plain Text</a></li>
                    <li><a href="/redirect">Redirect</a></li>
                </ul>
            </body>
        </html>
    """)

@app.get("/api/data")
def api_data() -> JSONResponse:
    return JSONResponse({
        "users": [
            {"id": 1, "name": "Alice"},
            {"id": 2, "name": "Bob"}
        ],
        "total": 2
    })

@app.get("/text")
def text_content() -> PlainTextResponse:
    return PlainTextResponse(
        "This is plain text content.\n"
        "It can span multiple lines."
    )

@app.get("/redirect")
def redirect_home() -> RedirectResponse:
    return RedirectResponse("/")

@app.post("/users")
def create_user(user) -> JSONResponse:
    return JSONResponse(
        {"id": 1, "name": user.get("name")},
        status_code=201
    )

@app.get("/error")
def error_response() -> JSONResponse:
    return JSONResponse(
        {"error": "Something went wrong"},
        status_code=500
    )

if __name__ == "__main__":
    app.serve("127.0.0.1", 8080)

Performance notes

All response types are implemented in Rust and converted to HTTP responses without leaving Rust-land, making them significantly faster than pure Python implementations.
Benchmarks:
Response TypeFastAPIFastrAPIImprovement
JSONResponse937 req/s31,360 req/s33x
HTMLResponse1,024 req/s33,200 req/s32x
PlainTextResponse1,150 req/s35,800 req/s31x
RedirectResponse1,089 req/s34,500 req/s32x

Next steps

Dependency injection

Learn about dependency injection

Architecture

Understand FastrAPI’s internal architecture

Build docs developers (and LLMs) love