Skip to content
Eraz1997Public

About

Kangaroos carry puppies in their pouches. `kangaroo` carries server-side data in static web applications, served by Axum.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

11 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Kangaroo 🦘

Kangaroos carry puppies in their pouches. kangaroo carries server-side data in static web applications, served by Axum.

This Rust crate allows you to serve static web applications and inject server-side data through Axum, making it available to the client as JSON objects. Kangaroo aims to avoid having heavy server runtimes serving apps through full SSR, but still be able to manipulate response status codes and reduce network round trips.

Usage πŸͺΏ

Setup πŸͺ›

cargo add kangaroo-axum

Basics 🍍

To easily get started, add the following in your existing Axum app as a bare minimum setup:

// Imports πŸ‘‡
use kangaroo_axum::{IntoKangarooError, KangarooConfig, KangarooRouterExtension, kangarooise};

// ...

// App extension πŸ‘‡
#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(get_home))
        // ...
        .with_kangaroo(KangarooConfig::new("public/"));
    //...
}

// ...

// Define your serialisable data model πŸ‘‡
#[derive(Serialize)]
struct HomeData {
    pub current_username: Option<String>,
}

// ...

// Kangarooise endpoint πŸ‘‡
#[kangarooise]
async fn get_home() -> HomeData {
    // ...
}

And in your frontend JavaScript code, add the following to retrieve the server-side injected data:

const data = JSON.parse(document.getElementById("kangaroo-data")?.textContent ?? null);

Requests to / will return the content of index.html with an injected JSON script with ID kangaroo-data containing your server-side data.

Single-page Applications πŸͺ¨

In single-page applications with client-side routing, Kangaroo data is fetched when a given URL is browsed, but not when navigation to that page is triggered by the client router or its links. Therefore, it's recommended to handle these cases by making the data consumable only once and fall back to APIs if it's already consumed.

For example, you could use a context in SolidJS:

type KangarooData = {
    getData: () => unknown;
};

const KANGAROO_DATA = JSON.parse(document.getElementById("kangaroo-data")?.textContent ?? null);
const KangarooContext = createContext<KangarooData | undefined>();

export function useKangaroo() {
    const context = useContext(KangarooContext);

    if (!context) {
        throw new Error("useKangaroo must be used inside KangarooProvider");
    }

    return context;
}

export const KangarooProvider = (props: { children: JSX.Element }) => {
	const [consumed, setIsConsumed] = createSignal<boolean>(false);

	return (
		<KangarooContext.Provider value={{ 
            getData: () => {
                const wasConsumed = consumed();
                setIsConsumed(true);
                return wasConsumed ? null : KANGAROO_DATA;
            }
        }}>
			{props.children}
		</KangarooContext.Provider>
	);
};

and then consume it with API fallback

const kangaroo = useKangaroo();
const [data] = createResource(async () => kangaroo.getData() ?? await (await fetch("/your/apis")).json());

Error Handling πŸ‘Ž

Kangaroo can gracefully handle errors raised by your endpoints:

#[kangarooise]
async fn get_home() -> Result<HomeData, Error> {
    // ...
}

You just need to implement the IntoKangarooError to make it handle gracefully. The only member must return status code and a payload to inject as kangaroo-data JSON script.

impl IntoKangarooError for Error {
    fn into_kangaroo_error(self) -> (StatusCode, impl Serialize) {
        // ...
    }
}

The frontend can read the serialised error payload from the kangaroo-data JSON script as usual.

Custom Paths πŸ‘Ÿ

Kangaroo supports some layers of customisation to pick the correct HTML document based on route and status code. The default document path is index.html, relative to the folder path passed to the KangarooConfig object.

To set another default:

KangarooConfig::new("public/")
    .with_default_document("default.html")

You can set specific document paths for each HTTP response status code coming from your Error objects:

use axum::http::StatusCode;

KangarooConfig::new("public/")
    .with_document_for_status(StatusCode::NOT_FOUND, "404.html")
    .with_document_for_status(StatusCode::INTERNAL_SERVER_ERROR, "5xx.html")

You can also set overrides for each route:

#[kangarooise(
    file = "home/index.html"
    not_found = "home/404.html"
    internal_server_error = "home/5xx.html"
)]
async fn get_home() -> Result<HomeData, Error> {
    // ...
}

file is the default path override for the route, the other arguments must be valid lowercased variants of the members of axum::http::StatusCode.

The precedence order for the document to load is the following, from top to bottom priority:

  1. Route-specific override for the returning status code
    #[kangarooise(not_found = "404.html")]
  2. Status code-specific default path
    KangarooConfig::new("public/")
        .with_document_for_status(StatusCode::NOT_FOUND, "404.html")
  3. Route-specific override for the default document
    #[kangarooise(file = "home.html")]
  4. Custom default document
    KangarooConfig::new("public/")
        .with_default_document("home.html")
  5. index.html

Frontend Development Servers πŸŽ’

If you're serving your frontend locally using a development server like Vite's, you can make Kangaroo forward requests to it instead of loading static files:

KangarooConfig::new("public/")
    .with_frontend_development_server(Some("http://127.0.0.1:3000"))

Since you'll access frontend pages through the backend proxy, hot module reloading may break, if in use. Therefore, make sure to set the frontend development server configuration properly to force HMR target the correct port. For instance, in Vite, you can set:

import { defineConfig } from "vite";

export default defineConfig({
	server: {
		port: 3000,
		hmr: {
			protocol: "ws",
			host: "localhost",
			port: 3000,
		},
	},
    // ...
});

Full Example πŸ’―

For a full simple working example, check out the examples folder.

Development πŸ‘¨β€πŸ’»

Setup πŸͺ›

Install Rust

Run Locally 🧸

cargo run --example app

Lint, Format and Test 🧽

cargo fmt --all
cargo clippy --workspace --all-targets -- --deny warnings
cargo test
RUSTDOCFLAGS="-D rustdoc::all -A rustdoc::private-doc-tests" cargo doc --all-features --no-deps

About

Kangaroos carry puppies in their pouches. `kangaroo` carries server-side data in static web applications, served by Axum.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages