I Trait My Best, I Swear

2026-01-28

For web servers, my go-to framework for Rust has been Rocket, and my favourite templating library has been the mustache crate.

This post covers some interesting things I discovered when trying to write a helper trait for my projects.

Rendering Mustache Templates

{{ Mustache }} is a specification for logic-less text templates, usually used for generating HTML. An example template might look like this:

<div class="card">
  <h2 class="card-title">{{title}}</h2>
  <hr>
  <p>{{content}}</p>
  <ul class="Tags">
  {{#tags}}
    <li>{{name}}</li>
  {{/tags}}
  </ul>
</div>

and the data used to fill out the template might look something like this:

{
  "title": "A Cool Article",
  "content": "This is a cool article full of fun stuff...",
  "tags": [ { "name": "cool"}, { "name": "fun" }]
}

The mustache crate for Rust represents compiled templates with the Template struct which can then be used to render the resulting HTML when given the required data for the template.

In our case, we'll use the render function to to render our templates. Any type that implement's serde's Serialize trait can be used here to supply the data to render templates.

For example:

use serde::Serialize;
#[derive(Serialize)]
struct Data {
  heading: String,
  content: String,
}
let template = mustache::compile_str(
  r#"
    <h1>{{heading}}</h1>
    <p>{{content}}</p>
  "#
).unwrap();
let mut bytes = vec![];
template.render(&mut bytes, &Data {
  heading: "Hello, Heading!".into(),
  content: "Some content to render.".into(),
}).unwrap();

The Goal

The idiomatic way to define route handlers in the rocket crate is to use macros like '#[get("/example")]' that decorate functions. These functions can then be mounted to parent routes. These functions can return any type that implements the Responder trait.

As we're interested in returning HTML, the RawHtml struct seems like what we should be returning. RawHtml wraps another type that implements Responder and sets the appropriate headers for returning raw HTML. We might use RawHtml<u8> then to render a template via:

#[get("/")]
fn index() -> RawHtml<u8> {
  #[derive(Serialize)]
  struct Data {
    title: String,
    content: String,
  }
  let template = mustache::compile_str(
    r#"
      <html>
        <body>
          <h1>{{title}}</h1>
          <p>{{content}}</p>
        </body>
      </html>
    "#
  ).unwrap();
  let mut bytes = vec![];
  template.render(&mut bytes, &Data {
    title: "Index".into(),
    content: "I wonder when I'll run out of example ideas?",
  }).unwrap();
  RawHtml(bytes)
}

What I'd like to do here, is be able to define a type here like Data, then implement a single trait for Data and be able to return Data from the responder. Something like the following.

trait Templated {
  const HTML: &str;
}

#[get("/")]
fn index() -> RawHtml<u8> {
  #[derive(Serialize)]
  struct Data {
    title: String,
    content: String,
  }
  impl Templated for Data {
    const HTML: &str = r#"
      <html>
        <body>
          <h1>{{title}}</h1>
          <p>{{content}}</p>
        </body>
      </html>
    "#;
  }
  Data {
    title: "Index".into(),
    content: "I wonder when I'll run out of example ideas?",
  }
}

This isn't possible, but I'd like to see just how close I can get.

The First Problem

The first 'problem' is Rust's orphan rule that requires for any implementation of a trait defined in a crate, either the trait or the type the trait is implemented for must be defined in that crate.

The orphan rule exists because otherwise you would need to specify the exact impl block you're referring to every time you use the functionality of a trait (which would be very messy).

To get around this we can just define a wrapper struct. Something like the following would do the trick.

struct Rendered<T: Templated>(T);

This actually is enough to to implement the trait we described and an example implementation is provided below.

use serde::Serialize;

use rocket::response::{Responder, content::RawHtml};

trait Templated: Serialize {
    const HTML: &str;
}

struct Rendered<T: Templated>(T);

impl<'r, T: Templated> Responder<'r, 'r> for Rendered<T> {
    fn respond_to(
        self,
        request: &'r rocket::Request<'_>,
    ) -> rocket::response::Result<'r> {
        let template =
            mustache::compile_str(T::HTML).unwrap();
        let mut bytes = vec![];
        template.render(&mut bytes, &self.0).unwrap();
        RawHtml(bytes).respond_to(request)
    }
}

This implementation does have one problem however...

Cache Me If You Can

The issue is with the call to mustache::compile_str that we make in our implementation. Here, we're recompiling the template every single time we render the HTML. What we'd like to do is to either precompile or lazily compile the templates.

Now, since we need a single instance of the template for each type we implement Templated for, the first thing that comes to mind might be one of the following:

trait Templated: Serialize {
  const HTML: &str;
  static TEMPLATE: mustache::Template =
        mustache::compile_str(Self::HTML).unwrap();
}
trait Templated: Serialize {
  const HTML: &str;
  const TEMPLATE: mustache::Template =
        mustache::compile_str(Self::HTML).unwrap();
}

Neither of these work however as associated statics are not allowed, and mustache::compile_str is not const.

We could try and get tricky with a const pointer to a static LazyLock or OnceLock. First we try the LazyLock:

trait Templated: Serialize {
    const HTML: &str;
    const TEMPLATE: &LazyLock<Template> = {
        static TEMPLATE: LazyLock<Template> =
            LazyLock::new(|| {
                mustache::compile_str(Self::HTML).unwrap()
            });
        &TEMPLATE
    };

This doesn't work however; we can't use the Self parameter from the outer item as the static is an entirely separate item than the item that contains it.

To get around this, we can switch to a OnceLock and put the code that initialises the cell in a place where we can get at Self.

use std::sync::OnceLock;

use mustache::Template;
use serde::Serialize;

use rocket::response::{Responder, content::RawHtml};

trait Templated: Serialize {
    const HTML: &str;
    const TEMPLATE: &OnceLock<Template> = {
        static TEMPLATE: OnceLock<Template> =
            OnceLock::new();
        &TEMPLATE
    };
}

struct Rendered<T: Templated>(T);

impl<'r, T: Templated> Responder<'r, 'r> for Rendered<T> {
    fn respond_to(
        self,
        request: &'r rocket::Request<'_>,
    ) -> rocket::response::Result<'r> {
        let template = T::TEMPLATE.get_or_init(|| {
            mustache::compile_str(T::HTML).unwrap()
        });
        let mut bytes = vec![];
        template.render(&mut bytes, &self.0).unwrap();
        RawHtml(bytes).respond_to(request)
    }
}

This seems to work initially but we hit a snag because as it turns out, there's actually only a single instance of the static template for all types. This is happening for the same reason we can't use Self in a static's definition; the static is a completely separate item from the item that contains it.

We encounter the problem even if we move the static into the respond_to function. So, what's the actual solution?

The Imperfect Solution

The best solution I've found to this problem is to define the trait like this:

trait Templated: Serialize {
    const HTML: &str;
    const TEMPLATE: &OnceLock<Template>;
    ///...
}

This looks similar, but notice we don't provide a default value for the TEMPLATE const. While this solves our problem, it sadly means that each implementation of the trait needs to provide a static reference to a unique OnceLock<Template>.

impl Templated for Page {
    const HTML: &str = r#"
        <h1>{{title}}</h1>
        <p>{{content}}</p>
    "#;

    const TEMPLATE: &OnceLock<Template> = {
        static TEMPLATE: OnceLock<Template> =
            OnceLock::new();
        &TEMPLATE
    };
}

This isn't optimal of course, but it does provide a unique Template for each type as we need. A full implementation of a basic webserver that uses this trait is provided below to show how it works.

#[macro_use]
extern crate rocket;

mod proto1;

use std::{io::Write, sync::OnceLock};

use mustache::Template;
use rocket::response::{Responder, content::RawHtml};
use serde::Serialize;

trait Templated: Serialize {
    const HTML: &str;
    const TEMPLATE: &OnceLock<Template>;
    fn template() -> &'static Template {
        Self::TEMPLATE.get_or_init(|| {
            mustache::compile_str(Self::HTML).unwrap()
        })
    }
    fn render<W: Write>(&self, w: &mut W) {
        Self::template().render(w, &self).unwrap()
    }
}

struct Rendered<T: Templated>(T);

impl<'r, T: Templated> Responder<'r, 'r> for Rendered<T> {
    fn respond_to(
        self,
        request: &'r rocket::Request<'_>,
    ) -> rocket::response::Result<'r> {
        let mut bytes = vec![];
        self.0.render(&mut bytes);
        RawHtml(bytes).respond_to(request)
    }
}

#[derive(serde::Serialize)]
struct Page {
    title: String,
    content: String,
}

impl Templated for Page {
    const HTML: &str = r#"
        <h1>{{title}}</h1>
        <p>{{content}}</p>
    "#;

    const TEMPLATE: &OnceLock<Template> = {
        static TEMPLATE: OnceLock<Template> =
            OnceLock::new();
        &TEMPLATE
    };
}

#[get("/")]
fn index() -> Rendered<Page> {
    Rendered(Page {
        title: "Hello, Title!".into(),
        content: "Here is some content.".into(),
    })
}

#[launch]
fn launch() -> _ {
    rocket::build().mount("/", routes![index])
}

What Did We Learn?

Types are far too hard,

I yearn for the days of bytes,

Time to go to bed.

Madeline Baggins