hyperchad_renderer_html_lambda 0.3.0

HyperChad HTML Lambda renderer package
Documentation

HyperChad HTML Lambda Renderer

AWS Lambda integration for HyperChad HTML renderer with serverless deployment support.

Overview

The HyperChad HTML Lambda Renderer provides:

  • AWS Lambda Integration: Full integration with AWS Lambda runtime
  • Serverless Deployment: Deploy HyperChad applications as serverless functions
  • HTTP Event Handling: Process API Gateway and ALB HTTP events
  • Response Compression: Automatic gzip compression for responses
  • Cold Start Optimization: Optimized for minimal cold start times
  • Error Handling: Comprehensive error handling and logging
  • JSON Support: Support for JSON API responses alongside HTML

Features

Lambda Runtime Support

  • Lambda HTTP: Integration with lambda_http crate
  • Event Processing: Handle API Gateway and ALB events
  • Response Generation: Generate proper Lambda HTTP responses
  • Error Handling: Lambda-compatible error responses

Response Features

  • HTML Responses: Server-rendered HTML pages
  • JSON Responses: API responses in JSON format (with json feature)
  • Raw Responses: Binary content with custom content-type
  • Compression: Automatic gzip compression for all responses
  • Headers: Custom header support

Performance Optimizations

  • Cold Start: Minimal initialization overhead
  • Memory Efficiency: Low memory footprint
  • Compression: Automatic gzip compression for reduced response sizes
  • Streaming Response API: Uses Lambda's streaming response API

Installation

Add this to your Cargo.toml:

[dependencies]
hyperchad_renderer_html_lambda = { path = "../hyperchad/renderer/html/lambda" }
hyperchad_template = { path = "../hyperchad/template" }  # For template DSL support

# Or explicitly enable JSON support (enabled by default):
# hyperchad_renderer_html_lambda = { path = "../hyperchad/renderer/html/lambda", features = ["json"] }

# Or explicitly enable asset serving (enabled by default):
# hyperchad_renderer_html_lambda = { path = "../hyperchad/renderer/html/lambda", features = ["assets"] }

Usage

Note: The examples below use the HyperChad template DSL (container! macro from hyperchad_template). The Lambda renderer supports Content::Html, Content::Raw, and Content::Json (with the json feature), and most users will use HyperChad templates to generate HTML content. You can also use raw HTML strings or any other templating approach that returns a String.

Basic Lambda Function

use hyperchad_renderer_html_lambda::{LambdaApp, LambdaResponseProcessor, Content};
use hyperchad_renderer::ToRenderRunner;
use hyperchad_template::container;
use lambda_http::Request;
use lambda_runtime::Error;
use bytes::Bytes;
use std::sync::Arc;

#[derive(Clone)]
struct MyLambdaProcessor;

#[async_trait::async_trait]
impl LambdaResponseProcessor<String> for MyLambdaProcessor {
    fn prepare_request(
        &self,
        req: Request,
        _body: Option<Arc<Bytes>>,
    ) -> Result<String, lambda_runtime::Error> {
        Ok(req.uri().path().to_string())
    }

    fn headers(&self, _content: &hyperchad_renderer::Content) -> Option<Vec<(String, String)>> {
        Some(vec![
            ("Content-Type".to_string(), "text/html; charset=utf-8".to_string()),
            ("Cache-Control".to_string(), "public, max-age=3600".to_string()),
            ("X-Powered-By".to_string(), "HyperChad".to_string()),
        ])
    }

    async fn to_response(
        &self,
        path: String,
    ) -> Result<Option<(Content, Option<Vec<(String, String)>>)>, lambda_runtime::Error> {
        match path.as_str() {
            "/" => Ok(Some((Content::Html(render_home_page()), None))),
            "/about" => Ok(Some((Content::Html(render_about_page()), None))),
            #[cfg(feature = "json")]
            "/api/health" => Ok(Some((
                Content::Json(serde_json::json!({"status": "ok"})),
                None
            ))),
            _ => Ok(None), // 404
        }
    }

    async fn to_body(
        &self,
        _content: hyperchad_renderer::Content,
        _data: String,
    ) -> Result<Content, lambda_runtime::Error> {
        unimplemented!("to_body is not used by the Lambda runtime")
    }
}

fn render_home_page() -> String {
    let view = container! {
        html {
            head {
                title { "HyperChad Lambda" }
                meta name="viewport" content="width=device-width, initial-scale=1";
            }
            body {
                div class="container" {
                    h1 { "Welcome to HyperChad on Lambda!" }
                    p { "This page is rendered by AWS Lambda." }

                    nav {
                        a href="/about" { "About" }
                        " | "
                        a href="/api/health" { "Health Check" }
                    }

                    div class="info" {
                        h2 { "Serverless Benefits" }
                        ul {
                            li { "Automatic scaling" }
                            li { "Pay per request" }
                            li { "Zero server management" }
                            li { "Global deployment" }
                        }
                    }
                }
            }
        }
    };

    view.to_string()
}

fn render_about_page() -> String {
    let view = container! {
        html {
            head {
                title { "About - HyperChad Lambda" }
            }
            body {
                div class="container" {
                    h1 { "About HyperChad Lambda" }
                    p { "HyperChad Lambda brings the power of HyperChad to serverless environments." }
                    a href="/" { "Back to Home" }
                }
            }
        }
    };

    view.to_string()
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    let processor = MyLambdaProcessor;
    let app = LambdaApp::new(processor);

    let mut runner = app.to_runner(hyperchad_renderer::Handle::current())?;
    runner.run().map_err(|e| Error::from(e.to_string()))?;

    Ok(())
}

API Gateway Integration

This example requires the json feature to be enabled.

#[cfg(feature = "json")]
mod api_example {
    use hyperchad_renderer_html_lambda::{LambdaApp, LambdaResponseProcessor, Content};
    use hyperchad_template::container;
    use lambda_http::Request;
    use lambda_runtime::Error;
    use bytes::Bytes;
    use std::sync::Arc;
    use serde::{Deserialize, Serialize};

    #[derive(Deserialize)]
    struct CreateUserRequest {
        name: String,
        email: String,
    }

    #[derive(Serialize)]
    struct CreateUserResponse {
        id: u64,
        name: String,
        email: String,
    }

    #[derive(Clone)]
    struct ApiProcessor;

    #[async_trait::async_trait]
    impl LambdaResponseProcessor<(String, String, Option<String>)> for ApiProcessor {
        fn prepare_request(
            &self,
            req: Request,
            body: Option<Arc<Bytes>>,
        ) -> Result<(String, String, Option<String>), lambda_runtime::Error> {
            let path = req.uri().path().to_string();
            let method = req.method().to_string();
            let body_str = body.and_then(|b| String::from_utf8(b.to_vec()).ok());

            Ok((method, path, body_str))
        }

        fn headers(&self, _content: &hyperchad_renderer::Content) -> Option<Vec<(String, String)>> {
            None // Content-Type is set automatically based on Content variant
        }

        async fn to_response(
            &self,
            (method, path, body): (String, String, Option<String>),
        ) -> Result<Option<(Content, Option<Vec<(String, String)>>)>, lambda_runtime::Error> {
            match (method.as_str(), path.as_str()) {
                ("GET", "/") => Ok(Some((Content::Html(render_api_docs()), None))),

                ("GET", "/api/users") => {
                    let users = vec![
                        serde_json::json!({"id": 1, "name": "Alice", "email": "alice@example.com"}),
                        serde_json::json!({"id": 2, "name": "Bob", "email": "bob@example.com"}),
                    ];
                    Ok(Some((Content::Json(serde_json::json!({"users": users})), None)))
                }

                ("POST", "/api/users") => {
                    if let Some(body) = body {
                        let request: CreateUserRequest = serde_json::from_str(&body)
                            .map_err(|e| lambda_runtime::Error::from(e.to_string()))?;

                        let response = CreateUserResponse {
                            id: 123,
                            name: request.name,
                            email: request.email,
                        };

                        Ok(Some((Content::Json(serde_json::to_value(response).unwrap()), None)))
                    } else {
                        Ok(Some((
                            Content::Json(serde_json::json!({"error": "Missing request body"})),
                            None
                        )))
                    }
                }

                _ => Ok(None), // 404
            }
        }

        async fn to_body(
            &self,
            _content: hyperchad_renderer::Content,
            _data: (String, String, Option<String>),
        ) -> Result<Content, lambda_runtime::Error> {
            unimplemented!("to_body is not used by the Lambda runtime")
        }
    }

    fn render_api_docs() -> String {
        let view = container! {
            html {
                head {
                    title { "API Documentation" }
                }
                body {
                    div class="container" {
                        h1 { "API Documentation" }

                        section {
                            h2 { "GET /api/users" }
                            p { "Returns a list of all users." }
                        }

                        section {
                            h2 { "POST /api/users" }
                            p { "Creates a new user." }
                            pre {
                                "{\n  \"name\": \"string\",\n  \"email\": \"string\"\n}"
                            }
                        }
                    }
                }
            }
        };

        view.to_string()
    }
}

Static Asset Serving

This example requires the assets feature to be enabled.

#[cfg(feature = "assets")]
mod asset_example {
    use hyperchad_renderer_html_lambda::{LambdaApp, LambdaResponseProcessor, Content};
    use hyperchad_renderer::assets::{StaticAssetRoute, AssetPathTarget};
    use hyperchad_template::container;
    use lambda_http::Request;
    use lambda_runtime::Error;
    use bytes::Bytes;
    use std::sync::Arc;

    #[derive(Clone)]
    struct AssetProcessor;

    #[async_trait::async_trait]
    impl LambdaResponseProcessor<String> for AssetProcessor {
        fn prepare_request(
            &self,
            req: Request,
            _body: Option<Arc<Bytes>>,
        ) -> Result<String, lambda_runtime::Error> {
            Ok(req.uri().path().to_string())
        }

        fn headers(&self, _content: &hyperchad_renderer::Content) -> Option<Vec<(String, String)>> {
            None
        }

        async fn to_response(
            &self,
            path: String,
        ) -> Result<Option<(Content, Option<Vec<(String, String)>>)>, lambda_runtime::Error> {
            match path.as_str() {
                "/css/style.css" => {
                    let css_content = b"body { margin: 0; }";
                    Ok(Some((
                        Content::Raw {
                            data: Bytes::from_static(css_content),
                            content_type: "text/css".to_string(),
                        },
                        Some(vec![("Cache-Control".to_string(), "public, max-age=86400".to_string())])
                    )))
                }
                "/" => Ok(Some((Content::Html(render_home_with_assets()), None))),
                _ => Ok(None),
            }
        }

        async fn to_body(
            &self,
            _content: hyperchad_renderer::Content,
            _data: String,
        ) -> Result<Content, lambda_runtime::Error> {
            unimplemented!("to_body is not used by the Lambda runtime")
        }
    }

    fn render_home_with_assets() -> String {
        let view = container! {
            html {
                head {
                    title { "HyperChad with Assets" }
                    link rel="stylesheet" href="/css/style.css";
                }
                body {
                    div class="container" {
                        h1 { "HyperChad with Static Assets" }
                        p { "This page includes CSS assets served from Lambda." }

                        div class="styled-content" {
                            p { "Static assets are bundled and served efficiently." }
                            ul {
                                li { "CSS stylesheets" }
                                li { "JavaScript files" }
                                li { "Images and fonts" }
                            }
                        }
                    }
                }
            }
        };

        view.to_string()
    }

    // Example of using the static_asset_routes field in LambdaApp
    fn create_app_with_routes() -> LambdaApp<String, AssetProcessor> {
        let mut app = LambdaApp::new(AssetProcessor);
        app.static_asset_routes = vec![
            StaticAssetRoute {
                route: "/css/style.css".to_string(),
                target: AssetPathTarget::FileContents(
                    Bytes::from_static(b"body { margin: 0; }")
                ),
                not_found_behavior: None,
            },
        ];
        app
    }
}

Environment-based Configuration

use hyperchad_renderer_html_lambda::{LambdaApp, LambdaResponseProcessor, Content};
use lambda_http::Request;
use lambda_runtime::Error;
use bytes::Bytes;
use std::{env, sync::Arc};

#[derive(Clone)]
struct ConfigurableProcessor {
    environment: String,
    debug: bool,
}

impl ConfigurableProcessor {
    fn new() -> Self {
        Self {
            environment: env::var("ENVIRONMENT").unwrap_or_else(|_| "production".to_string()),
            debug: env::var("DEBUG").is_ok(),
        }
    }

    fn render_home(&self) -> String {
        format!(
            r#"<html>
                <head><title>HyperChad - {}</title></head>
                <body>
                    <h1>HyperChad Lambda</h1>
                    <p>Environment: {}</p>
                </body>
            </html>"#,
            self.environment, self.environment
        )
    }
}

#[async_trait::async_trait]
impl LambdaResponseProcessor<String> for ConfigurableProcessor {
    fn prepare_request(
        &self,
        req: Request,
        _body: Option<Arc<Bytes>>,
    ) -> Result<String, lambda_runtime::Error> {
        Ok(req.uri().path().to_string())
    }

    fn headers(&self, _content: &hyperchad_renderer::Content) -> Option<Vec<(String, String)>> {
        let mut headers = vec![
            ("X-Environment".to_string(), self.environment.clone()),
        ];

        if self.debug {
            headers.push(("X-Debug".to_string(), "true".to_string()));
        }

        Some(headers)
    }

    async fn to_response(
        &self,
        path: String,
    ) -> Result<Option<(Content, Option<Vec<(String, String)>>)>, lambda_runtime::Error> {
        match path.as_str() {
            "/" => Ok(Some((Content::Html(self.render_home()), None))),
            _ => Ok(None),
        }
    }

    async fn to_body(
        &self,
        _content: hyperchad_renderer::Content,
        _data: String,
    ) -> Result<Content, lambda_runtime::Error> {
        unimplemented!("to_body is not used by the Lambda runtime")
    }
}

Deployment

SAM Template

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
    HyperChadFunction:
        Type: AWS::Serverless::Function
        Properties:
            CodeUri: target/lambda/hyperchad-lambda/
            Handler: provided
            Runtime: provided.al2
            Architectures:
                - x86_64
            Events:
                Api:
                    Type: Api
                    Properties:
                        Path: /{proxy+}
                        Method: ANY
                Root:
                    Type: Api
                    Properties:
                        Path: /
                        Method: ANY
            Environment:
                Variables:
                    RUST_LOG: info
                    ENVIRONMENT: production

Build Script

#!/bin/bash
# Build for Lambda
cargo build --release --target x86_64-unknown-linux-musl

# Create deployment package
mkdir -p target/lambda/hyperchad-lambda
cp target/x86_64-unknown-linux-musl/release/hyperchad-lambda target/lambda/hyperchad-lambda/bootstrap

# Deploy with SAM
sam build
sam deploy --guided

Feature Flags

  • json: Enable JSON response support (enabled by default)
  • assets: Enable static asset serving support (enabled by default)
  • debug: Enable debug mode features (enabled by default)

Performance Optimizations

Cold Start Reduction

  • Minimal Dependencies: Only essential dependencies included
  • Binary Size: Optimized for fast cold starts

Memory Efficiency

  • Compression: Automatic gzip compression for all responses
  • Binary Responses: Efficient binary body handling

Dependencies

  • lambda_http: AWS Lambda HTTP event handling
  • lambda_runtime: AWS Lambda runtime integration
  • hyperchad_renderer: Core HTML rendering functionality
  • flate2: Gzip compression support
  • bytes: Efficient byte buffer handling
  • async-trait: Async trait support
  • serde_json: JSON serialization (optional, enabled by default)

Integration

This renderer is designed for:

  • Serverless Web Apps: Full serverless web applications
  • API Services: REST and JSON APIs
  • Server-Rendered Pages: Dynamic HTML page generation
  • Microservices: Event-driven microservices architecture

Limitations

  • Cold Starts: Initial request latency for cold starts
  • Execution Time: 15-minute maximum execution time
  • Memory: Limited memory allocation options
  • State: No persistent state between invocations