Welcome

Hi! Welcome to the documentation for Trillium, a modular toolkit for building async rust web applications.

Trillium runs on stable rust, is fully async, and can run on tokio, async-std, or smol. Using Trillium starts with code as simple as this:

fn main() {
    trillium_smol::run(|conn: trillium::Conn| async move {
        conn.ok("hello from trillium!")
    });
}

Trillium is also built to scale up to complex applications with a full middleware stack comparable to Rails or Phoenix. Currently, opt-in features include a router, cookies, sessions, websockets, serving static files from disk or memory, a reverse proxy, and integrations for three template engine options. Trillium is just getting started, though, and there's a lot more to build.

Perhaps most importantly, Trillium intends to be a production-quality open source http framework for async rust, with support options available for commercial users.

Trillium's code is at github and rustdocs are available at docs.trillium.rs.

About this document

Here are some conventions in this document.

  • use declarations will only be listed once on the first usage of a given type in order to keep code samples concise
  • In-line code looks like this: |conn: Conn| async move { conn } and will generally not involve fully qualified paths
  • Footnotes are represented like this1
  • Informational asides look like this:

    ℹ️ Fun fact: Facts are fun

  • Advanced asides look like this

    πŸ§‘β€πŸŽ“ The handler trait provides several other lifecycle hooks for library authors

  • Comparisons with Tide

    🌊 Tide endpoints look like |_req: Request<_>| async { Response::new(200) } whereas Trillium handlers look like |conn: Conn| async move { conn.with_status(200) }

  • Comparisons with Plug:

    πŸ”Œ Halting a plug looks like conn |> halt (elixir), and the equivalent in trillium is returning conn.halt()

1

Footnotes can always be skipped

Who is this document for?

This document expects some familiarity with async rust. We intend to offer a beginner level document at some point, but for now we recommend looking at the rust book and the async book.

We also assume familiarity with web development in general, including concepts and patterns in http servers and frameworks.

In particular, we offer comparisons to rust's tide and elixir's phoenix / plug, as they serve as the primary inspirations for trillium.

Architectural Overview

Composition and Substitution

Trillium is published as a set of components that can be easily composed to create web servers. One of the goals of this design is that to the extent possible, all components be replaceable by alternatives.

Why is substitution so important?

Async rust web frameworks still have a lot of exciting exploration left in the near future. Instead of offering one solution as the best, trillium offers a playground in which you can experiment with alternatives. I want it to be painless to plug in an alternative router, or a different http logger, or anything else you can imagine.

There are a lot of different purposes a web framework might be used for, and the core library should not have to adapt in order for someone to add support for each of those features.

Although I imagine that for each of the core components there will only be one or two options, I think it is an essential aspect of good software design that frameworks be modular and composable, as there will always be tradeoffs for any given design.

Only compile what you need

Instead of your application depending on a library with a large list of reexported dependencies and conditionally including/excluding them based on cargo features, trillium tries to apply rust's "only pay for what you need" approach both at runtime and compile time. In particular, trillium avoids pulling in runtimes like tokio or async-std except in the crates where you explicitly need those, preferring instead to depend on small crates like futures_lite wherever possible. Additionally, and in specific contrast to tide, there is minimal default behavior. If you don't need a router, you don't need to compile or run a router.

Everything is opt-in, instead of opt-out. Trillium uses small crates, each of which declares its own dependencies.

Relation to tide, http-types, and async-h1

As of trillium-v0.2.0, trillium no longer depends on http-types.

Trillium shares the same session store backends as tide.

Relation to Elixir Plug and Phoenix

The general architecture is directly inspired by Plug, and is intended to be a hybrid of the best of plug and the best of tide. Eventually, I intend to build an opinionated framework like Phoenix on top of the components that are Trillium, but I don't expect that to happen for a bit. I hope to keep the core feature set of trillium quite small and focus on getting the design right and improving performance as much as possible.

Core concepts: Handlers, Conn, and Adapters

The most important concepts when building a trillium application are the Conn type and the Handler trait. Each Conn represents a single http request/response pair, and a Handler is the trait that all applications, middleware, and endpoints implement.

Let's start with an overview of a simple trillium application and then dig into each of those concepts a little more.

fn main() {
    trillium_smol::run(|conn: trillium::Conn| async move {
        conn.ok("hello from trillium!")
    });
}

In this example, trillium_smol::run is the runtime adapter and the closure is a Handler that responds "hello from trillium!" to any web request it receives. This is a fully functional example that you can run with only the following dependencies:

[dependencies]
trillium = "0.2"
trillium-smol = "0.2"

If we cargo run this example, we can then visit http://localhost:8080 in a browser or make a curl request against that url and see "hello from trillium!" as the response body. Note that we won't see any output in the terminal because trillium is silent by default.

Handlers

The simplest form of a handler is any async function that takes a Conn and returns that Conn. This example sets a 200 Ok status and sets a string body.

use trillium::Conn;
async fn hello_world(conn: Conn) -> Conn {
    conn.ok("hello world!")
}

With no further modification, we can drop this handler into a trillium server and it will respond to http requests for us. We're using the smol-runtime based server adapter here.

pub fn main() {
    trillium_smol::run(hello_world);
}

We can also define this as a closure:

pub fn main() {
    trillium_smol::run(|conn: trillium::Conn| async move {
        conn.ok("hello world")
    });
}

This handler will respond to any request regardless of path, and it will always send a 200 Ok http status with the specified body of "hello world".

The State Handler

Trillium offers only one handler in the main trillium crate: The State handler, which places a clone of any type you provide into the state set of each conn that passes through it. See the rustdocs for State for example usage.

Tuple Handlers

Earlier, we discussed that we can use state to send data between handlers and that handlers can always pass along the conn unchanged. In order to use this, we need to introduce the notion of tuple handlers.

Each handler in a tuple handler is called from left to right until the conn is halted.

πŸ”Œ Readers familiar with elixir plug will recognize this notion as identical to pipelines, and that the term halt is stolen from inspired by plug

env_logger::init();
use trillium_logger::Logger;
run((
    Logger::new(),
    |conn: Conn| async move { conn.ok("tuple!") }
));

This snippet adds a http logger to our application, so that if we execute our application with RUST_LOG=info cargo run and make a request to http://localhost:8000, we'll see log output on stdout.

πŸ§‘β€πŸŽ“β“ Why not vectors or arrays? Rust vectors and arrays are type homogeneous, so in order to store the Logger and closure type in the above example in an array or vector, we'd need to allocate them to the heap and actually store a smart pointer in our homogeneous collection. Trillium initially was built around a notion of "sequences," which were a wrapper around Vec<Box<dyn Handler + 'static>>. Because tuples are generic over each of their elements, they can contain heterogeneous elements of different sizes, without heap allocation or smart pointers.

Implementing Handler

The rustdocs for Handler contains the full details of the Handler interface for library authors. For many applications, it will not be necessary to use anything other than an async function or closure, but Handler can contain its own state and be implemented for any type that you author.

Assorted implementations provided by the trillium crate

You may see a few other types used in tests and examples.

  • (): the noop handler, identical to |conn: Conn| async move { conn }
  • &'static str and String: This simple handler responds to all conns by halting, setting a 200-ok status, and sending the string content as response body. trillium_smol::run("hello") is identical to trillium_smol::run(|conn: Conn| async move { conn.ok("hello") })
  • Option<impl Handler>: This handler will noop if the option variant is none. This is useful for conditionally including handlers at runtime based on configuration or environment.

Conn

Before we explore the concept of a handler further, let's take a look at Conn. As mentioned above, Conn represents both the request and response, as well as any data your application associates with that request-response cycle.

πŸ§‘β€πŸŽ“ Advanced aside: Although the naming of Conn is directly borrowed from Elixir's plug and therefore also Phoenix, it does in fact also own (in the rust sense) the singular TcpStream that represents the connection with the http client, and dropping a Conn will also disconnect the client as a result.

The rustdocs for Conn contain the full details for all of the things you can do with a conn.

Returning Conn

In general, because you'll be returning Conn from handlers, it supports a chainable (fluent) interface for setting properties, like:

conn.with_status(202)
    .with_response_header("content-type", "application/something-custom")
    .with_body("this is my custom body")

Accessing http request properties

Conn also contains read-only properties like request headers, request path, and request method, each of which have getter associated functions.

Default Response

The default response for a Conn is a 404 with no response body, so it is always valid to return the Conn from a handler unmodified (|conn: Conn| async move { conn } is the simplest valid handler).

State

In addition to holding the request properties and accumulating the response your application is going to send, a Conn also serves as a data structure for any information your application needs to associate with that request. This is especially valuable for communicating between handlers, and most core handlers are implemented using conn state. One important caveat to is that each Conn can only contain exactly one of each type, so it is highly recommended that you only store types that you define in state.

🌊 Comparison with Tide: Tide has three different types of state: Server state, request state, and response state. In Trillium, server state is achieved using the trillium::State handler, which holds any type that is Clone and puts a clone of it into the state of each Conn that passes through the handler.

Extending Conn

It is a very common pattern in trillium for libraries to extend Conn in order to provide additional functionality1. The Conn interface does not provide support for sessions, cookies, route params, or many other building blocks that other frameworks build into the core types. Instead, to use sessions as an example, trillium_sessions provides a SessionConnExt trait which provides associated functions for Conn that offer session support. In general, handlers that put data into conn state also will provide convenience functions for accessing that state, and will export a [Something]ConnExt trait.

1 πŸ§‘β€πŸŽ“ see library_patterns for an example of authoring one of these

Runtime Adapters and TLS

Runtime Adapters

Let's talk a little more about that trillium_smol::run line we've been writing. Trillium itself is built on futures (futures-lite, specifically). In order to run it, it needs an adapter to an async runtime. There there are four of these currently:

Although we've been using the smol adapter in these docs thus far, you should use whichever runtime you prefer. If you expect to have a dependency on async-std or tokio anyway, you might as well use the adapter for that runtime. If you're new to async rust or don't have an opinion, I recommend starting with trillium_smol. It is easy to switch trillium between runtimes at any point.

12-Factor by default, but overridable

Trillium seeks to abide by a 12 factor approach to configuration, accepting configuration from the environment wherever possible. The number of configuration points that can be customized through environment variables will likely increase over time.

To run trillium on a different host or port, either provide a HOST and/or PORT environment variables, or compile the specific values into the application as follows:

pub fn main() {
    trillium_smol::config()
        .with_port(1337)
        .with_host("127.0.0.1")
        .run(|conn: trillium::Conn| async move { conn.ok("hello world") })
}

In addition to accepting the HOST and PORT configuration from the environment, on cfg(unix) systems, trillium will also pick up a LISTEN_FD environment variable for use with catflap/systemfd. On cfg(unix) systems, if the HOST begins with ., /, or ~, it is interpreted as a path and bound as a unix domain socket.

For more documentation on the default values and what configuration can be chained onto config(), see trillium_server_common::Config.

TLS / HTTPS

With the exception of aws lambda, which provides its own tls termination at the load balancer, each of the above servers can be combined with either rustls or native-tls, or with trillium-acme to register a certificate automatically with an ACME certificate provider like Let's Encrypt.

Rustls:

rustdocs (main)

use trillium::Conn;
use trillium_rustls::RustlsAcceptor;

const KEY: &[u8] = include_bytes!("./key.pem");
const CERT: &[u8] = include_bytes!("./cert.pem");

pub fn main() {
    env_logger::init();
    trillium_smol::config()
        .with_acceptor(RustlsAcceptor::from_single_cert(CERT, KEY))
        .run(|conn: Conn| async move { conn.ok("ok") });
}

Native tls:

rustdocs (main)

use trillium::Conn;
use trillium_native_tls::NativeTlsAcceptor;

pub fn main() {
    env_logger::init();
    let acceptor = NativeTlsAcceptor::from_pkcs12(include_bytes!("./identity.p12"), "changeit");
    trillium_smol::config()
        .with_acceptor(acceptor)
        .run(|conn: Conn| async move { conn.ok("ok") });
}

Automatic HTTPS via Let's Encrypt:

See the trillium-acme documentation for examples.

A tour of some of the handlers that exist today

In order for trillium to be a usable web framework, we offer a number of core utilities. However, it is my hope that alternative implementations for at least some of these will exist in order to explore the design space and accommodate different design constraints and tradeoffs. Because not every application will need this functionality, they are each released as distinct crates from the core of trillium.

Router

rustdocs (main)

The trillium router is based on routefinder. This router supports two types of patterns: Untyped params and a single wildcard. Named params are captured in a map-like interface. Any handler can be mounted inside of a Router (including other Routers), allowing entire applications to be mounted on a path, and allowing for tuple handlers to be run on a given route. Any handler mounted inside of a route that includes a * will have the url rewritten to the contents of that star.

Alternative routers that are not based on routefinder are a prime opportunity for innovation and exploration.

Here's a simple example of an application that responds to a request like http://localhost:8000/greet/earth with "hello earth" and http://localhost:8000/greet/mars with "hello mars" and responds to http://localhost:8000 with "hello everyone"

use trillium::Conn;
use trillium_router::{Router, RouterConnExt};

pub fn main() {
    trillium_smol::run(
        Router::new()
            .get("/", |conn: Conn| async move { conn.ok("hello everyone") })
            .get("/hello/:planet", |conn: Conn| async move {
                let planet = conn.param("planet").unwrap();
                let response_body = format!("hello {planet}");
                conn.ok(response_body)
            }),
    );
}

Nesting

Trillium also supports nesting routers, making it possible to express complex sub-applications, vaguely along the lines of a rails engine. When there are additional types of routers, it will be possible for an application built with one type of router to be published as a crate and nested inside of another router as long as they depend on a compatible version of the trillium crate.

use trillium::{conn_try, conn_unwrap, Conn, Handler};
use trillium_logger::Logger;
use trillium_router::{Router, RouterConnExt};

struct User {
    id: usize,
}

mod nested_app {
    use super::*;
    async fn load_user(conn: Conn) -> Conn {
        let id = conn_try!(conn.param("user_id").unwrap().parse(), conn);
        let user = User { id }; // imagine we were loading a user from a database here
        conn.with_state(user)
    }

    async fn greeting(mut conn: Conn) -> Conn {
        let user = conn_unwrap!(conn.take_state::<User>(), conn);
        conn.ok(format!("hello user {}", user.id))
    }

    async fn post(mut conn: Conn) -> Conn {
        let user = conn_unwrap!(conn.take_state::<User>(), conn);
        let body = conn_try!(conn.request_body_string().await, conn);
        conn.ok(format!("hello user {}, {}", user.id, body))
    }

    async fn some_other_route(conn: Conn) -> Conn {
        conn.ok("this is an uninspired example")
    }

    pub fn handler() -> impl Handler {
        (
            load_user,
            Router::new()
                .get("/greeting", greeting)
                .get("/some/other/route", some_other_route)
                .post("/post", post),
        )
    }
}
pub fn main() {
    env_logger::init();
    trillium_smol::run((
        Logger::new(),
        Router::new()
            .get("/", |conn: Conn| async move { conn.ok("hello everyone") })
            .any(&["get", "post"], "/users/:user_id/*", nested_app::handler()),
    ));
}

Template engines

There are currently three template engines for trillium. Although they are in no way mutually exclusive, most applications will want at most one of these.

Askama

Askama is a jinja-based template engine that preprocesses templates at compile time, resulting in efficient and type-safe templates that are compiled into the application binary. Here's how it looks:

Given the following file in (cargo root)/templates/examples/hello.html,

Hello, {{ name }}!
use trillium::Conn;
use trillium_askama::{AskamaConnExt, Template};

#[derive(Template)]
#[template(path = "examples/hello.html")]
struct HelloTemplate<'a> {
    name: &'a str,
}

fn main() {
    trillium_smol::run(|conn: Conn| async move { conn.render(HelloTemplate { name: "world" }) });
}

rustdocs (main)

Ructe

Ructe is a compile-time typed template system similar to askama, but using a build script instead of macros.

  • crate: https://crates.io/crates/trillium-ructe
  • repository: https://github.com/prabirshrestha/trillium-ructe
  • docs: https://docs.rs/trillium-ructe/latest/trillium_ructe/

Tera

Tera offers runtime templating. Trillium's tera integration provides an interface very similar to phoenix or rails, with the notion of assigns being set on the conn prior to render.

Given the following file in the same directory as main.rs (examples in this case),

Hello, {{ name }}!
use trillium::Conn;
use trillium_tera::{TeraConnExt, TeraHandler};

fn main() {
    trillium_smol::run((TeraHandler::new("**/*.html"), |conn: Conn| async move {
        conn.assign("name", "hi").render("examples/hello.html")
    }));
}

rustdocs (main)

Handlebars

Handlebars also offers runtime templating. Given the following file in examples/templates/hello.hbs,

hello {{name}}!
use trillium::Conn;
use trillium_handlebars::{HandlebarsConnExt, HandlebarsHandler};

fn main() {
    env_logger::init();
    trillium_smol::run((
        HandlebarsHandler::new("./examples/templates/*.hbs"),
        |conn: Conn| async move {
            conn.assign("name", "world")
                .render("examples/templates/hello.hbs")
        },
    ));
}

rustdocs (main)

Logger

rustdocs (main)

use trillium::{Conn, State};
use trillium_logger::{apache_combined, Logger};

#[derive(Clone, Copy)]
struct User(&'static str);

impl User {
    pub fn name(&self) -> &'static str {
        self.0
    }
}

fn user_id(conn: &Conn, _color: bool) -> &'static str {
    conn.state::<User>().map(User::name).unwrap_or("-")
}

pub fn main() {
    trillium_smol::run((
        State::new(User("jacob")),
        Logger::new().with_formatter(apache_combined("-", user_id)),
        "ok",
    ));
}

Cookies

rustdocs (main)

use trillium::Conn;
use trillium_cookies::{CookiesConnExt, CookiesHandler};

pub fn main() {
    env_logger::init();

    trillium_smol::run((CookiesHandler::new(), |conn: Conn| async move {
        if let Some(cookie_value) = conn.cookies().get("some_cookie") {
            println!("current cookie value: {}", cookie_value.value());
        }

        conn.with_cookie(("some_cookie", "some-cookie-value"))
            .ok("ok!")
    }));
}

Sessions

rustdocs (main)

Sessions are a common convention in web frameworks, allowing for a safe and secure way to associate server-side data with a given http client (browser). Trillium's session storage is built on the async-session crate, which allows us to share session stores with tide. Currently, these session stores exist:

1

The memory store and cookie store should be avoided for use in production applications. The memory store will lose all session state on server process restart, and the cookie store makes different security tradeoffs than the database-backed stores. If possible, use a database.

❗The session handler must be used in conjunction with the cookie handler, and it must run after the cookie handler. This particular interaction is also present in other frameworks, and is due to the fact that regardless of which session store is used, sessions use a secure cookie as a unique identifier.

use trillium::Conn;
use trillium_cookies::CookiesHandler;
use trillium_sessions::{MemoryStore, SessionConnExt, SessionHandler};

pub fn main() {
    env_logger::init();

    trillium_smol::run((
        CookiesHandler::new(),
        SessionHandler::new(MemoryStore::new(), "01234567890123456789012345678901123"),
        |conn: Conn| async move {
            let count: usize = conn.session().get("count").unwrap_or_default();
            conn.with_session("count", count + 1)
                .ok(format!("count: {count}"))
        },
    ));
}

Proxy

Trillium includes a custom http client implementation in order to support reverse proxying requests. There are two tls implementations for this client.

use trillium_client::Client;
use trillium_logger::Logger;
use trillium_proxy::{
    upstream::{ConnectionCounting, IntoUpstreamSelector, UpstreamSelector},
    Proxy,
};
use trillium_smol::ClientConfig;

pub fn main() {
    env_logger::init();
    let upstream = if std::env::args().count() == 1 {
        "http://localhost:8080".into_upstream().boxed()
    } else {
        std::env::args()
            .skip(1)
            .collect::<ConnectionCounting<_>>()
            .boxed()
    };

    trillium_smol::run((
        Logger::new(),
        Proxy::new(
            Client::new(ClientConfig::default()).with_default_pool(),
            upstream,
        )
        .with_via_pseudonym("trillium-proxy"),
    ));
}

Static file serving

Trillium offers two rudimentary approaches to static file serving for now. Neither of these approaches perform any cache-related header checking yet.

From disk

This handler loads content from disk at request, and does not yet do any in-memory caching.

rustdocs (main)

#[cfg(unix)]
pub fn main() {
    use trillium_static::{crate_relative_path, files};
    trillium_smol::run((
        trillium_logger::logger(),
        files(crate_relative_path!("examples/files")).with_index_file("index.html"),
    ))
}

#[cfg(not(unix))]
pub fn main() {}

From memory, at compile time

This handler includes all of the static content in the compiled binary, allowing it to be shipped independently from the assets.

rustdocs (main)

#[cfg(unix)]
pub fn main() {
    use trillium_static_compiled::static_compiled;

    trillium_smol::run((
        trillium_logger::Logger::new(),
        trillium_caching_headers::CachingHeaders::new(),
        static_compiled!("./examples/files").with_index_file("index.html"),
    ));
}

#[cfg(not(unix))]
pub fn main() {}

WebSocket support

rustdocs (main)

use futures_util::StreamExt;
use trillium_logger::logger;
use trillium_websockets::{websocket, Message, WebSocketConn};

async fn websocket_handler(mut conn: WebSocketConn) {
    while let Some(Ok(Message::Text(input))) = conn.next().await {
        let result = conn
            .send_string(format!("received your message: {}", &input))
            .await;

        if let Err(e) = result {
            log::error!("{e}");
            break;
        }
    }
}

pub fn main() {
    env_logger::init();
    trillium_smol::run((logger(), websocket(websocket_handler)));
}

🌊 WebSockets work a lot like tide's, since I recently wrote that interface as well. One difference in trillium is that the websocket connection also contains some aspects of the original http request, such as request headers, the request path and method, and any state that has been accumulated by previous handlers in a sequence.

Testing trillium applications

Trillium provides a testing crate that intends to provide both "functional/unit testing" and "integration testing" of trillium applications.

rustdocs (main)

Given a totally-contrived application like this:

use trillium::{conn_try, Conn, Handler, KnownHeaderName};
use trillium_logger::Logger;

async fn teapot(mut conn: Conn) -> Conn {
    let request_body = conn_try!(conn.request_body_string().await, conn);
    if request_body.is_empty() {
        conn.with_status(406).with_body("unacceptable!").halt()
    } else {
        conn.with_body(format!("request body was: {request_body}"))
            .with_status(418)
            .with_response_header(KnownHeaderName::Server, "zojirushi")
    }
}

fn application() -> impl Handler {
    (Logger::new(), teapot)
}

fn main() {
    trillium_smol::run(application());
}

Here's what some simple tests would look like:

#[cfg(test)]
mod tests {
    use super::{application, teapot};
    use trillium_testing::prelude::*;

    #[test]
    fn handler_sends_correct_headers_and_is_a_teapot() {
        let application = application();
        assert_response!(
            post("/").with_request_body("hello trillium!").on(&application),
            Status::ImATeapot,
            "request body was: hello trillium!",
            "server" => "zojirushi",
            "content-length" => "33"

        );
    }

    #[test]
    fn we_can_also_test_the_individual_handler() {
        assert_body!(
            post("/").with_request_body("a different body").on(&teapot),
            "request body was: a different body"
        );
    }

    #[test]
    fn application_is_lemongrab_when_body_is_empty() {
        let application = application();
        assert_response!(
            post("/").on(&application),
            Status::NotAcceptable,
            "unacceptable!"
        );
    }
}

Patterns for library authors

State

Let's take a look at an implementation of a library that incrementally counts the number of conns that pass through it and attaches the number to each conn. It would be unsafe to store a u64 directly in the state set, because other libraries might be doing so, so we wrap it with a private newtype called ConnNumber. Since this isn't accessible outside of our library, we can be sure that our handler is the only place that sets it. We provide a ConnExt trait in order to provide access to this data.

mod conn_counter {
    use std::sync::{
        atomic::{AtomicU64, Ordering},
        Arc,
    };
    use trillium::{async_trait, Conn, Handler};

    struct ConnNumber(u64);

    #[derive(Default)]
    pub struct ConnCounterHandler(Arc<AtomicU64>);

    impl ConnCounterHandler {
        pub fn new() -> Self {
            Self::default()
        }
    }

    #[async_trait]
    impl Handler for ConnCounterHandler {
        async fn run(&self, conn: Conn) -> Conn {
            let number = self.0.fetch_add(1, Ordering::SeqCst);
            conn.with_state(ConnNumber(number))
        }
    }

    pub trait ConnCounterConnExt {
        fn conn_number(&self) -> u64;
    }

    impl ConnCounterConnExt for Conn {
        fn conn_number(&self) -> u64 {
            self.state::<ConnNumber>()
                .expect("conn_number must be called after the handler")
                .0
        }
    }
}

And usage of the library looks like this:

use conn_counter::{ConnCounterConnExt, ConnCounterHandler};
use trillium::{Conn, Handler};

fn handler() -> impl Handler {
    (ConnCounterHandler::new(), |conn: Conn| async move {
        let conn_number = conn.conn_number();
        conn.ok(format!("conn number was {conn_number}"))
    })
}

fn main() {
    trillium_smol::run(handler());
}

#[cfg(test)]
mod test {
    use trillium_testing::prelude::*;

    #[test]
    fn test_conn_counter() {
        let handler = super::handler();
        assert_ok!(get("/").on(&handler), "conn number was 0");
        assert_ok!(get("/").on(&handler), "conn number was 1");
        assert_ok!(get("/").on(&handler), "conn number was 2");
        assert_ok!(get("/").on(&handler), "conn number was 3");
    }
}

Contributing

If you're excited about the ideas here, this page will be kept up to date with ways to get involved.

  1. Build stuff using trillium. Open source if possible, but even if not, feedback from actual deployed applications will be given higher priority than issues that aren't driven by real use cases.
  2. Build new handlers for trillium. The intent of trillium's design is that as many components as possible should be replaceable. It would make me very happy to deprecate one of the components I built in preference to a more robust alternative. All trillium-compatible crates will be linked in a section of the documentation.
  3. Contribute to the documentation and tests
  4. File issues for bugs

❗Please don't file a Pull Request, regardless of how small, without prior discussion on an Issue. All PRs without an associated issue will be immediately closed. I value your time and want to make sure that any code you write is in an aligned direction.