Introduction

Auric is an Ember-inspired web application framework for building modern applications in Rust that compiles to WASM. Auric emphasizes the MVC separation of concerns, where:

  • Visual Designers maintain an application skin using HTMLbars templates
  • Developers maintain controllers, components, models, and adapters that keep code out of templates

An Ember Experience in Rust

Auric provides a way for you to write single-page applications using your favorite Ember design patterns, including Models, Views (templates), Controllers, Components, and Adapters, using type-safe async Rust.

Auric's build process combines those assets into a code-generated Leptos webapp that compiles to WASM for superior performance and security.

Key Technologies Used

Auric integrates the following key technologies:

  • HTMLbars (re-imagined in Rust), for offering developers a rich set of expression and data binding functionality within their Handlebars templates.
  • Ember Data (re-imagined in Rust), for in-memory model lifecycle management, and for JSON:API based relational storage middleware that makes it possible to implement adapters for REST, JSON:API, GraphQL, gRPC, or anything else.
  • Trunk, for WASM bundling, and for supporting development sessions with live page-reload
  • Leptos, for rendering and interactivity

Installing

First ensure Rust is installed. Details here.

Next, ensure the wasm32-unknown-unknown Rust platform target is installed so that Rust can compile to WebAssembly.

$ rustup target add wasm32-unknown-unknown

Next, install the Auric CLI:

$ cargo install auric

Finally, install Trunk:

$ cargo install --locked trunk

You're now ready to create your first Auric application:

$ auric new my-app
==> make
2026-08-13T14:20:39.039679Z  INFO 🚀 Starting trunk 0.21.14
2026-08-13T14:20:40.474657Z  INFO 📦 starting build
   ...
   Compiling auric-build v0.2.0
   Compiling auric-runtime v0.2.0
   Compiling handlebars v6.4.4
   Compiling js-sys v0.3.103
   Compiling console_error_panic_hook v0.1.7
   Compiling web-sys v0.3.104
   Compiling leptos v0.8.4
   Compiling my-app v0.1.0 (/Users/davidr/workspaces/my-app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 14.25s

$ cd my-app
$ auric serve
==> trunk serve
2026-07-04T14:57:49.311838Z  INFO 🚀 Starting trunk 0.21.14
2026-07-04T14:57:49.385672Z  INFO 📦 starting build
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s
2026-07-04T14:57:49.734082Z  INFO applying new distribution
2026-07-04T14:57:49.734841Z  INFO ✅ success
2026-07-04T14:57:49.734883Z  INFO 📡 serving static assets at -> /
2026-07-04T14:57:49.734931Z  INFO 📡 server listening at:
2026-07-04T14:57:49.734935Z  INFO     🏠 http://127.0.0.1:4200/
2026-07-04T14:57:49.734937Z  INFO     🏠 http://[::1]:4200/
2026-07-04T14:57:49.735070Z  INFO     🏠 http://localhost.:4200/

Templates

Templates contain Handlebars markup laying out a snippet of HTML.

The Application Template

Every Auric application has an application template at src/templates/application.hbs, and its contents are displayed at all times. This template establishes the outer-most structure of your application layout.

The default content looks like this:

<h1>Welcome to Auric SPA!</h1>
{{outlet}}

When you build your application, a Leptos component is generated for the application template. This code representation of a template is called the View.

$ make
$ cat src/generated/views/application.rs
// This file is @generated by auric-build.

use auric_runtime::{*, components::*, data::*, routing::*};
use leptos::prelude::*;
use leptos_router::{path, components::*};

use crate::generated::views::{
    index::IndexView,
};

#[component]
pub fn ApplicationView() -> impl IntoView {
    view! {
        <Router>
            <h1>Welcome to Auric SPA!</h1>
            <div />
            
            <Routes fallback=|| "Not found.">
                <Route path=path!("/") view=IndexView />
                <Route path=path!("/*any") view=NotFound />
            </Routes>
        </Router>
    }
}

#[component]
fn NotFound() -> impl IntoView {
    view! { <h1>"Not found."</h1> }
}

The Index Template

Every Auric application also has an implicit Index route, and associated template containing default {{outlet}} content. This is required by the Leptos router, which needs an IndexView to associate with the root route having path="/".

The implicit index template can be replaced with an explicit one, if you need to control the content:

$ auric generate template index
created src/templates/index.hbs

And likewise, the implicit index route can be replaced with an explicit one:

$ auric generate route index
created src/routes/index_route.rs
updated src/routes/mod.rs
created src/templates/index.hbs

Other Templates

Let's define a new route to display a list of customers:

$ auric generate route customers
created src/routes/customers_route.rs
updated src/routes/mod.rs
created src/templates/customers.hbs
==> make
   Compiling my-app v0.1.0 (/Users/davidr/workspaces/my-app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.70s

The default content for a new template is simply {{outlet}}. But let's update the template to display a list of customers:

<h2>Customers:</h2>

{{#each this.model.customers as |customer|}}
    <li>{{customer.name}}</li>
{{/each}}

The Route at src/routes/customers.rs would then define a model() function to load the list of customers from the Store.

When you build your application, the Leptos CustomersView component is generated, and looks like this:

$ make
$ cat src/generated/views/customers.rs
...
#[component]
pub fn CustomersView() -> impl IntoView {
    view! {
        <h2>Customers:</h2>
        ...
    }
}

Performance and Security

You might have noticed that Handlebars templates are only used at compile-time, and they are not bundled within your web application's wasm bundle, or rendered at runtime. Instead, they are translated at build time into Leptos components, which are compiled and statically linked into your wasm bundle. This design ensures that views are hardened, performant, and async.

Components

A component is like your own custom HTML tag. It provides a way for you to centralize and re-use a snippet of HTML.

Let's generate a component to layout a customer record using a Bootstrap card class:

$ auric generate component customer-card
created src/components/customer_card.hbs
==> make
   Compiling ...
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.91s

The initial content of src/components/customer_card.hbs is simply {{yield}}. Let's customize it:

<div class="card text-center">
    <div class="card-body">
        {{yield}}
    </div>
</div>

If we rebuild our project, we can see that a Leptos component was generated for our component at src/generated/components/customer_card.rs:

$ make
   Compiling ...
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.91s

$ cat src/generated/components/customer_card.rs
use leptos::prelude::*;

#[component]
pub fn CustomerCardComponent(children: Children) -> impl IntoView {
    view! {
        <div class="card text-center">
            <div class="card-body">
                {children()}
            </div>
        </div>
    }
}

Now let's reference our new customer card component from another view:

{{!-- src/templates/customers.hbs --}}
<CustomerCard>
    Acme Widgets
</CustomerCard>

<CustomerCard>
    Standard Paper Supplies, Inc.
</CustomerCard>

If we rebuild our project, we can examine the Leptos component that was generated for the customers view, which now contains the references to the customer card component:

$ make
   Compiling ...
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.91s

$ cat src/generated/views/customers.rs
// This file is @generated by auric-build.

use auric_runtime::{*, components::*, data::*, routing::*};
use leptos::prelude::*;
use leptos_route::{path, components::*};

use crate::generated::components::{
    customer_card::CustomerCardComponent,
};

#[component]
pub fn CustomersView() -> impl IntoView {
    view! {
        <CustomerCardComponent>
            Acme Widgets
        </CustomerCardComponent>
        
        <CustomerCardComponent>
            Standard Paper Supplies, Inc.
        </CustomerCardComponent>
    }
}

Component Arguments and HTML Attributes

Component Arguments

Components can be given named arguments, like this:

{{!-- src/components/customer_card.hbs --}}
<div class="card text-center">
    <div class="card-body">
        <h5 class="card-title">{{ @name }}</h5>
        <p class="card-text">
            {{yield}}
        </p>
    </div>
</div>

And then using the component from a template looks like this:

<CustomerCard @name="Acme Widgets">
    We ship to your door.
</CustomerCard>

In the generated code, the datatype of an argument is a JSON Value type, derived from the calling template. From the example above, @name="Acme Widgets" informs the Auric code generator that the name argument is a String:

// src/generated/components/customer_card.rs
// This file is @generated by auric-build.

use auric_runtime::components::*;
use leptos::prelude::*;

#[component]
pub fn CustomerCardComponent(
    children: Children,
    name: String,
) -> impl IntoView {
    view! {
        <div class="card text-center">
            <div class="card-body">
                <h5 class="card-title">{name}</h5>
                <p class="card-text">
                    {children()}
                </p>
            </div>
        </div>
    }
}

HTML Attributes

When using a component from a template, you can also pass through attributes such as class or anything else, like this:

<CustomerCard class="text-center" @name="Acme Widgets">
    We ship to your door.
</CustomerCard>

And the generated view with the calling code ends up looking like this:

// src/generated/view/customers.rs
// This file is @generated by auric-build.

use auric_runtime::components::*;
use leptos::prelude::*;

#[component]
pub fn CustomersView() -> impl IntoView {
    ...
    view! {
        <CustomerCardComponent attr:class="text-center" name="Acme Widgets".to_string()>
            "We ship to your door."
        </CustomerCardComponent>
    }
}

Conditional Content

In HTMLbars templates, you can use if to conditionally render content.

Inline If

The inline-if syntax takes a condition parameter, a valueIfTrue parameter, and an optional valueIfFalse parameter. Here's an example of the usage:

The car is {{if this.model.isVeryNice 'very' 'not very'}} nice.

What gets generated during a build:

#[component]
pub fn CarView() -> impl IntoView {
    ...
    let signal0 = move || model.get().map(|model| model.get("isVeryNice")).unwrap_or_default();
    let signal1 = move || {if (signal0()).as_bool().unwrap_or_default() {"very"} else {"not very"}};

    view! {
        "The car is " {move || signal1()} "."
    }
}

Block If

When you have nested content, use the block-if syntax.

The car is
{{#if this.model.isVeryNice}}
    <strong>very nice</strong>
{{else}}
    nice
{{/if}}.

What gets generated during a build:

#[component]
pub fn CarView() -> impl IntoView {
    ...
    let signal0 = move || model.get().map(|model| model.get("isVeryNice")).unwrap_or_default();

    view! {
        "The car is "
        <Show when=move || { (&signal0()).as_bool().unwrap_or_default() } fallback=|| view! { "nice" }>
            <strong>"very nice"</strong>
        </Show>
        "."
    }
}

Helpers

Helpers are pre-packaged Rust functions that you can call within your HTMLbars expressions.

In the Ember ecosystem, certain helper packages became popular and are commonly used by many apps. These are made available to Auric webapps. The ones you don't use should be stripped out by the Rust compiler.

How They Work

Helpers streamline your development experience by helping to keep your templates clean, highly readable, and maintainable.

Consider this HTMLbars template:

{{!-- src/templates/customer.hbs --}}
<h2>Customer</h2>

<li>Name: {{titleize (trim this.model.name)}}</li>
<li>Address: {{trim this.model.address}}</li>

When you build your Auric project, let's look at the Leptos view that is generated for you, and some of its complexities that you won't have to face directly:

$ make
$ cat src/generated/views/customer.rs
// This file is @generated by auric-build.

use auric_runtime::{*, components::*, data::*, routing::*};
use leptos::prelude::*;
use leptos_router::{path, components::*};

#[component]
pub fn CustomerView() -> impl IntoView {
    let maybe_model = LocalResource::new(async move || {
        let router = use_context::<Arc<Router>>().expect("Router not found");
        let params = move || use_params_map().get();
        router.set_params_for("customer", params().into());
        router.model_for("customer").await
    });
    let model = Signal::derive(move || maybe_model.get().and_then(Result::ok));
    let model_error = move || maybe_model.get().map(|result| result.map(|_| ()));
    let signal0 = move || model.get().map(|model| model.get("name")).unwrap_or_default();
    let signal1 = move || auric_runtime::helpers::string::trim(&signal0());
    let signal2 = move || auric_runtime::helpers::string::titleize(&signal1());
    let signal3 = move || -> String { signal2().into() };
    let signal4 = move || model.get().map(|model| model.get("address")).unwrap_or_default();
    let signal5 = move || auric_runtime::helpers::string::trim(&signal4());
    let signal6 = move || -> String { signal5().into() };

    view! {
        <h2>"Customer"</h2>
        <li>"Name: " {move || signal3()} </li>
        <li>"Address: " {move || signal6()} </li>
    }
}

ember-cli-string-helpers

The following ember-cli-string-helpers are supported:

camelize, capitalize, classify, dasherize, humanize, lowercase, titleize, trim, truncate, underscore, uppercase, and w.

ember-math-helpers

The following ember-math-helpers are supported:

abs, acos, acosh, add, asin, asinh, atan, atan2, atanh, cbrt, ceil, clz32, cos, cosh, div, exp, expm1, floor, fround, gcd, hypot, imul, lcm, log-e, log1p, log2, log10, max, min, mod, mult, pow, random, round, sign, sin, sqrt, sub, sum, tan, tanh, and trunc.

ember-truth-helpers

The following ember-truth-helpers are supported:

HelperRust OperationHTMLbars Usage
andif (a && b){{if (and a b)}}
eqif (a == b){{if (eq a b)}}
gtif (a > b){{if (gt a b)}}
gteif (a >= b){{if (gte a b)}}
is-arrayif (serde_json::Value::is_array()){{if (is-array a)}}
is-empty...lengthy type-specific logic...{{if (is-empty a)}}
is-equal(identical to eq){{if (is-equal a b)}}
ltif (a < b){{if (lt a b)}}
lteif (a <= b){{if (lte a b)}}
notif (!a){{if (not a)}}
not-eqif (a != b){{if (not-eq a b)}}
orif (a || b){{if (or a b)}}
xorif (a ^ b){{if (xor a b)}}

Routing

A URL can be set in the following ways:

  • The user loads the app for the first time.
  • The user changes the URL manually, by clicking on a back or forward button, or by editing the address bar.
  • The user clicks on a link within the app.

When the URL changes, the Auric framework:

  • finds the route handler that you have provided for the given route
  • calls its model() function
  • renders the route's template, which can include references to the model

Defining Routes

When your application starts, the Leptos router matches the current URL to the routes that you've defined. The routes, in turn, are responsible for loading data, and setting up application state.

Basic Routes

Let's generate a new route:

$ auric generate route invoices
created src/routes/invoices_route.rs
updated src/routes/mod.rs
created src/templates/invoices.hbs
==> make
   Compiling my-app v0.1.0 (/Users/davidr/workspaces/my-app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.73s

This will generate the following file:

// src/routes/invoices_route.rs
use anyhow::Result;
use auric_runtime::async_trait::async_trait;
use auric_runtime::routing::Route;
use auric_runtime::{AuricRoute, Context};
use serde_json::{Value, json};

#[derive(AuricRoute, Default)]
#[route("/invoices")]
pub struct InvoicesRoute {}

#[async_trait]
impl Route for InvoicesRoute {
    fn init(&mut self, _context: &dyn Context) -> Result<()> {
        Ok(())
    }

    async fn model(&self, _params: RouteParams) -> Result<RouteModel> {
        Ok(json!({}).into())
    }
}

#[cfg(test)]
mod tests {
    use super::InvoicesRoute;

    #[test]
    fn can_construct() {
        let _ = InvoicesRoute::default();
    }
}

The Auric build process also generates an ApplicationView that includes the Leptos Router and its Route definitions. Let's have a look:

// src/generated/views/application.rs
...
#[component]
pub fn ApplicationView() -> impl IntoView {
    view! {
        <Router>
            <Routes fallback=|| "Not found.">
                <Route path=path!("/") view=IndexView />
                <Route path=path!("/invoices") view=InvoicesView />
                <Route path=path!("/*any") view=|| view! { <h1>Not found.</h1> } />
            </Routes>

            <h1>Welcome to Auric SPA!</h1>
            <div />
        </Router>
    }
}

Nested Routes

Nested routes are used when you want to have a template that displays within another template. They are automatically generated for you when you create a hierarchy of routes such as this:

$ auric generate route customers/new
created src/routes/customers/new_route.rs
updated src/routes/customers/mod.rs
updated src/routes/mod.rs
created src/templates/customers/new.hbs
==> make
   Compiling auric-runtime v0.1.8 (/Users/davidr/workspaces/auric-spa/auric/crates/auric-runtime)
   Compiling my-app v0.1.0 (/Users/davidr/workspaces/my-app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.93s

Examine the generated application view to see the nested route definitions:

$ cat src/generated/views/application.rs
...
#[component]
pub fn ApplicationView() -> impl IntoView {
    ...
    view! {
        <Router>
            <Routes fallback=|| "Not found.">
                <Route path=path!("/") view=IndexView />
                <ParentRoute path=path!("/customers") view=CustomersView>
                    <Route path=path!("/new") view=CustomersNewView />
                </ParentRoute>
                <Route path=path!("/*any") view=|| view! { <h1>Not found.</h1> } />
            </Routes>

            ...
        </Router>
    }
}

Index Routes

At every level of nesting, Auric automatically provides an implicit route for the index path. Implicit routes use the pre-defined view=DefaultView from auric_runtime::components. You can examine the generated application view for the details:

$ cat src/generated/views/application.rs 
...
#[component]
pub fn ApplicationView() -> impl IntoView {
    ...
    view! {
        <Router>
            <h1>"Welcome to Auric SPA!"</h1>
            <div />

            <Routes fallback=|| "Not found.">
                <Route path=path!("/") view=IndexView />
                <Route path=path!("/customer/:customer_id") view=CustomerView />
                <ParentRoute path=path!("/customers") view=CustomersView>
                    <Route path=path!("") view=DefaultView />  <== IMPLICIT ROUTE
                    <Route path=path!("/new") view=CustomersNewView />
                </ParentRoute>
                <Route path=path!("/*any") view=|| view! { <h1>Not found.</h1> } />
            </Routes>
        </Router>
    }
}

You can also define an explicit index route, to control the model() function or for other purposes:

$ auric generate route customers/index
created src/routes/customers/index_route.rs
created src/templates/customers/index.hbs
==> make
   Compiling auric-build v0.2.0 (/Users/davidr/workspaces/auric-spa/auric/crates/auric-build)
   Compiling auric-runtime v0.2.0 (/Users/davidr/workspaces/auric-spa/auric/crates/auric-runtime)
   Compiling my-app v0.1.0 (/Users/davidr/workspaces/my-app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.29s

$ cat src/generated/views/application.rs
...
#[component]
pub fn ApplicationView() -> impl IntoView {
    ...
    view! {
        <Router>
            <h1>"Welcome to Auric SPA!"</h1>
            <div />

            <Routes fallback=|| "Not found.">
                <Route path=path!("/") view=IndexView />
                <Route path=path!("/customer/:customer_id") view=CustomerView />
                <ParentRoute path=path!("/customers") view=CustomersView>
                    <Route path=path!("") view=CustomersIndexView />  <== EXPLICIT ROUTE 
                    <Route path=path!("/new") view=CustomersNewView />
                </ParentRoute>
                <Route path=path!("/*any") view=|| view! { <h1>Not found.</h1> } />
            </Routes>
        </Router>
    }
}

Dynamic Segments

One of the responsibilities of a route handler is to load a model. The route handler struct needs to define the necessary URL parameter within the #[route] attribute macro.

This example shows a route that loads a customer record:

// src/routes/customer_route.rs
use crate::generated::Bucket;
use crate::models::customer::Customer;
use anyhow::Result;
use auric_runtime::data::Store;
use auric_runtime::routing::{Route, RouteModel, RouteParams};
use auric_runtime::{AuricRoute, Context, async_trait};
use jsonapi_core::Resource;
use std::sync::Arc;

#[derive(AuricRoute, Default)]
#[route("/customer/:customer_id")]
pub struct CustomerRoute {
    store: Arc<Store>,
}

#[async_trait(?Send)]
impl Route for CustomerRoute {
    fn init(&mut self, context: &dyn Context) -> Result<()> {
        self.store = context.store();
        Ok(())
    }

    async fn model(&self, params: RouteParams) -> Result<RouteModel> {
        let id = params.get("customer_id").expect("Expected customer_id in params");
        let customer: Customer = self.store.find_record(Bucket::Customers, id, None).await?;
        Ok(Resource::from_typed(&customer)?.into())
    }
}

Linking Between Routes

Establishing links to different parts of your application can be done with the LinkTo component. It takes a @route parameter, which specifies the dot-delimited name of the route to link to.

Let's link to an invoices page from a navbar in our application template:

// src/templates/application.hbs
<h1>Links:</h1>
<LinkTo @route="customers">
    <button class="btn btn-default">Customers</button>
</LinkTo>
<LinkTo @route="invoices">
    <button class="btn btn-default">Invoices</button>
</LinkTo>
{{outlet}}

This will generate the following file:

// src/generated/application.rs
...
#[component]
pub fn ApplicationView() -> impl IntoView {
    view! {
        <Router>
            <Routes fallback=|| "Not found.">
                ...
            </Routes>

            <h1>Links:</h1>
            <LinkToComponent path="customers".to_string()>
                <button class="btn btn-default">Customers</button>
            </LinkToComponent>
            <LinkToComponent path="invoices".to_string()>
                <button class="btn btn-default">Invoices</button>
            </LinkToComponent>
            <div />
        </Router>
    }
}

Dynamic Segments

The LinkTo component also supports a @model parameter, which is required to navigate to a route with a dynamic segment, such as /customer/123.

<LinkTo @route="customer" @model="123">
    <button class="btn btn-default">Customer 123</button>
</LinkTo>

You can also use a HTMLbars expression:

<LinkTo @route="customer" @model="{{ this.model.id }}">
    <button class="btn btn-default">Customer 123</button>
</LinkTo>

NOTE:

The HTMLbars expression within the mustache requires double-quotes around it, whereas in Ember the quotes are not required. I hope this inconvenience is temporary.

Specifying a Route's Model

You define a route's model by implementing its model() function.

Static Models

Let's return a simple model:

// src/routes/customers_route.rs
...

#[async_trait]
impl Route for CustomersRoute {
    ...
    async fn model(&self, _params: RouteParams) -> Result<RouteModel> {
        Ok(json!({
            "name": "Acme Widgets",
            "totalMonthlySpend": 2300,
        }).into())
    }
}

You can now reference the model values from your route template:

{{!-- src/templates/customers.hbs --}}

<li>Name: {{this.model.name}}</li>
<li>Total Monthly Spend: {{this.model.totalMonthlySpend}}</li>

Fetching Data

To fetch data from your model() function, first customize your route initialization to save a reference to the application-wide Store:

// src/routes/customers_route.rs

#[derive(AuricRoute, Default)]
#[route("/customers")]
pub struct CustomersRoute {
    store: Arc<Store>,
}

#[async_trait]
impl Route for CustomersRoute {
    fn init(&mut self, context: &dyn Context) -> Result<()> {
        self.store = context.store();
        Ok(())
    }
}

Now you can use the store from your model() function:

// src/routes/customers_route.rs

use crate::generated::Bucket;

#[async_trait]
impl Route for CustomersRoute {
    ...
    async fn model(&self, _params: RouteParams) -> Result<RouteModel> {
        let customers: Vec<Customer> = self.store.find_all(Bucket::Customers, None).await?;
        Ok(RouteModel::from(customers))
    }
}

See Routing / Defining Routes / Dynamic Segments for an example model() function that returns a single record using an id URL parameter.

Controllers

A controller handles the business logic for a given route. If you have a customers/new route that needs custom logic such as an event handler, you'll want to create a customers/new controller to hold the event handling functions as well as any potential tracked values.

Defining a Controller

$ auric generate controller customers/new
created src/controllers/customers/new_controller.rs
updated src/controllers/customers/mod.rs
updated src/controllers/mod.rs
==> make
   Compiling auric-build v0.2.2 (/Users/davidr/workspaces/auric-spa/auric/crates/auric-build)
   Compiling auric-derive v0.2.2 (/Users/davidr/workspaces/auric-spa/auric/crates/auric-derive)
   Compiling auric-runtime v0.2.2 (/Users/davidr/workspaces/auric-spa/auric/crates/auric-runtime)
   Compiling my-app v0.1.0 (/Users/davidr/workspaces/my-app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.30s

Here's what the new controller looks like in its initial state:

use anyhow::Result;
use auric_runtime::{AuricController, Context, Controller, async_trait, controller};

#[derive(AuricController, Default)]
pub struct CustomersNewController {}

#[async_trait(?Send)]
impl Controller for CustomersNewController {
    fn init(&mut self, _context: &dyn Context) -> Result<()> {
        Ok(())
    }
}

#[controller]
impl CustomersNewController {}

#[cfg(test)]
mod tests {
    use super::CustomersNewController;

    #[test]
    fn can_construct() {
        let _ = CustomersNewController::default();
    }
}

Adding a tracked value

To add a tracked value, add it to your controller struct, and decorate it with the #[tracked] derive macro, like this:

#[derive(AuricController, Default)]
pub struct CustomersNewController {
    #[tracked]
    counter: u16,
}

When you build your project, a corresponding Leptos signal will be generated into the view that corresponds with your controller:

// src/generated/views/customers_new.rs
let (counter, set_counter) = signal::<u16>(Default::default()); // tracked from controller

view! {
...

Handling Events

This example shows how to write an event handler to increment a tracked property, then display the latest value.

Let's start with the template:

<h2>Counter Example</h2>

Counter: {{this.counter}}

<button class="btn btn-default" {{on "click" this.increment}}>
    Increment
</button>

Next, we need to add an event handler function to our controller:

#[controller]
impl CustomersNewController {
    pub fn increment(&mut self) {
        self.counter += 1;
    }
}

That's it! Let's look at the Leptos view that was generated:

// This file is @generated by auric-build.

use auric_runtime::{*, components::*, routing::*};
use leptos::prelude::*;
use leptos_router::{components::*, hooks::use_params_map};
use std::sync::Arc;

#[component]
pub fn CustomersNewController() -> impl IntoView {
    let config = use_context::<serde_json::Value>().expect("Config not found");
    let store = use_context::<Arc<auric_runtime::data::Store>>().expect("Store not found");
    let ctx = InitContext::new(config.clone(), store.clone());
    let mut controller = crate::controllers::customers::new_controller::CustomersNewController::default();
    controller.init(&ctx).expect("failed initializing controller");
    let maybe_model = LocalResource::new(async move || {
        let router = use_context::<Arc<Router>>().expect("Router not found");
        let params = move || use_params_map().get();
        router.set_params_for("customers.new", params().into());
        router.model_for("customers.new").await
    });
    let model = Signal::derive(move || maybe_model.get().and_then(Result::ok));
    let model_error = move || maybe_model.get().map(|result| result.map(|_| ()));
    let (counter, set_counter) = signal::<u16>(Default::default()); // tracked from controller

    view! {
        <h2>"Counter Example"</h2>
        "Counter: "
        {move || counter.get()}
        <button
            class="btn btn-default"
            on:click=move |_| {
                controller.set_counter(&counter);
                controller.increment();
                set_counter.set(*controller.get_counter());
            }
        >
            "Increment"
        </button>
    }
}

Controllers provide the means by which you can keep your templates lean and manageable, thanks to the power of HTMLbars.

With Auric, also notice that the path references within mustache expressions, such as the {{this.counter}} tracked variable reference or the {{on "click" this.increment}} event handler function, are statically bound to real Rust variables and functions at compile time. Any mustache reference to a non-existent field, such as {{this.doesNotExist}}, is a compile error.

Auric leverages the Rust compiler to fully validate your templates.

Data Framework

Auric has a data framework that is equivalent to Ember Data, enabling you to define Models that work with a back-end through adapters for REST, JSON:API, GraphQL, gRPC, or anything else.

Defining the Application Adapter

The first step in setting up data access is to generate an Application adapter. This establishes a default adapter for the Store to use, which can be supplemented later with additional model-specific adapters.

Let's generate a REST-based Application adapter:

$ auric generate adapter application --target rest
created src/adapters/application.rs
updated src/adapters/mod.rs
==> make
   Compiling my-app v0.1.0 (/Users/davidr/workspaces/my-app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.47s

This will create the following file:

// src/adapters/application.rs
use auric_runtime::{
    Config,
    async_trait::async_trait,
    data::{Adapter, QueryRecordOptions, adapters::RestAdapter},
};
use jsonapi_core::Resource;

#[derive(Default)]
pub struct ApplicationAdapter {
    target: RestAdapter,
}

#[async_trait(?Send)]
impl Adapter for ApplicationAdapter {
    fn init(&mut self, config: &Config) -> Result<()> {
        self.target.init(config)
    }

    async fn create_record(&self, resource: Resource) -> Result<Resource> {
        self.target.create_record(resource).await
    }

    async fn delete_record(&self, resource: &Resource) -> Result<()> {
        self.target.delete_record(resource).await
    }

    async fn find_record(&self, resource_type: &str, id: &str) -> Result<Resource> {
        self.target.find_record(resource_type, id).await
    }

    async fn query(&self, resource_type: &str, query: serde_json::Value) -> Result<Vec<Resource>> {
        self.target.query(resource_type, query).await
    }

    async fn query_record(
        &self,
        resource_type: &str,
        query: serde_json::Value,
        options: QueryRecordOptions,
    ) -> Result<Option<Resource>> {
        self.target
            .query_record(resource_type, query, options)
            .await
    }

    async fn update_record(&self, resource: Resource) -> Result<Resource> {
        self.target.update_record(resource).await
    }
}

#[cfg(test)]
mod tests {
    use super::ApplicationAdapter;
    use auric_runtime::data::Adapter;
    use serde_json::json;

    #[test]
    fn can_construct() {
        let _ = ApplicationAdapter::default();
    }

    #[test]
    fn can_init() {
        let mut adapter = ApplicationAdapter::default();
        let config = json!({"api_url": "http://localhost:3000"});
        adapter
            .init(&config)
            .expect("Expected to initialize with a valid api url");
    }

    #[test]
    fn cannot_init_with_missing_api_url() {
        let mut adapter = ApplicationAdapter::default();
        let config = json!({"someStuff": "abc"});
        assert!(
            adapter.init(&config).is_err(),
            "Expected init to fail due to missing api url",
        );
    }
}

As you can see, the implementation of each Adapter trait function simply delegates to a target RestAdapter, providing a default implementation upfront, while leaving you in a position to make future customizations.

There is also a JsonApiAdapter, that you can target with:

$ auric generate adapter application --target json-api

Defining Models

A model struct defines the properties and behavior of the data presented to your user.

Generating a Model

Let's generate a new model:

$ auric generate model customer
created src/models/customer.rs
updated src/models/mod.rs
==> make
   Compiling my-app v0.1.0 (/Users/davidr/workspaces/my-app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.47s

This will generate the following file:

// src/models/customer.rs
use auric_runtime::AuricModel;
use jsonapi_core::JsonApi;

#[derive(AuricModel, Clone, Debug, Default, JsonApi, PartialEq)]
#[jsonapi(type = "customers")]
pub struct Customer {
    #[jsonapi(id)]
    pub id: String,
}

#[cfg(test)]
mod tests {
    use super::Customer;

    #[test]
    fn can_construct() {
        let _ = Customer::default();
    }
}

The full range of model features are documented in the jsonapi_core crate.

Adding Fields and Relations

Let's add a few additional fields:

// src/models/customer.rs
use auric_runtime::AuricModel;
use jsonapi_core::{JsonApi, Relationship};
use super::account::Account;

#[derive(AuricModel, Clone, Debug, Default, JsonApi, PartialEq)]
#[jsonapi(type = "customers")]
pub struct Customer {
    #[jsonapi(id)]
    pub id: String,
    
    pub name: String,
    
    #[jsonapi(relationship, type = "accounts")]
    pub owner: Relationship<Account>,
}

The Bucket Enum

A Bucket enum is generated at src/generated/bucket.rs during the build process, so that the Rust compiler can validate your resource type, and so that you do not have to worry about when to singularize, pluralize, dasherize, or camelize it.

// This file is @generated by auric-build.

use strum_macros::AsRefStr;

#[derive(AsRefStr, Clone, Debug)]
pub enum Bucket {
    #[strum(serialize = "customers")]
    Customers,

    #[strum(serialize = "invoiceLineItems")]
    InvoiceLineItems,
}
let customer: Customer = store.find_record(Bucket::Customers, "123", None).await?;

Finding Records

The Store provides functions for retrieving records of a single type.

See Fetching Data in the routing guide for how to obtain a reference to the application-wide store.

Retrieving a Single Record

Use Store::find_record() to retrieve a record by its type and ID.

use crate::generated::Bucket;
use crate::models::customer::Customer;

let customer: Customer = store.find_record(Bucket::Customers, "123", None).await?;

Use Store::peek_record() to retrieve a record by its type and ID, without making a network request. This will return the record only if it is already present in the store's cache:

let maybe_customer: Option<Customer> = store.peek_record(Bucket::Customers, "123")?;

Retrieving Multiple Records

Use Store::find_all() to retrieve all of the records for a given type:

let customers: Vec<Customer> = store.find_all(Bucket::Customers, None).await?;

Use Store::peek_all() to retrieve all of the records for a given type that are already loaded in the store, without making a network request:

let customers: Vec<Customer> = store.peek_all(Bucket::Customers)?;

Querying for Multiple Records

Use Store::query() to query for records that meet certain criteria.

use serde_json::json;

let customers: Vec<Customer> = store.query(Bucket::Customers, json!({
    "filter": {
        "name": "Acme Widgets",
    },
})).await?;

Creating, Updating, and Deleting Records

See Fetching Data in the routing guide for how to obtain a reference to the application-wide Store.

Creating Records

Use Store::create_record() or Store::save() to create a record.

use crate::generated::Bucket;
use crate::models::customer::Customer;

let customer = Customer{
    name: "Acme Widgets".to_string(),
    ..Default::default()
};

let finalized_customer = store.create_record(Bucket::Customers, &customer).await?;
log::info!("The assigned customer id is {}", finalized_customer.id);

Updating Records

Use Store::save() to store an updated record.

let mut customer: Customer = store.find_record(Bucket::Customers, "123", None).await?;
customer.name = "New Name".to_string();
let customer = store.save(&customer).await?;

Deleting Records

Use Store::delete_record() to delete a record:

let customer: Customer = store.find_record(Bucket::Customers, "123", None).await?;
store.delete_record(&customer).await?;

Pushing Records into the Store

The Store provides a cache of all the previously loaded records. This enables dynamic routing, including back-navigation, to see a consistent set of records, whether dirty or not, which has always been one of Ember's core strengths.

Records can be pushed into the cache ahead of time, to avoid later network operations.

See Fetching Data in the routing guide for how to obtain a reference to the application-wide Store.

Pushing Records

Use Store::push() to push a record into the store.

use crate::models::customer::Customer;

let customer = Customer{
    id: "123".to_string(),
    name: "Acme Widgets".to_string(),
    ..Default::default()
};
store.push(customer)?;

Application Concerns

Configuring your Application

Configuration is performed within the src/config.rs file, which defines a configuration factory function that returns a JSON object.

To customize your application build, use the env! and option_env! Rust macros to read environment variables at build time. This will hard-code their final values into your sealed wasm bundle, ensuring scrict release versioning and optimal security.

Configuring your Adapter

One common use case is the need to configure your application's adapters with the URL of your back-end API service. To achieve that, your configuration might look like this:

// src/config.rs
pub fn new() -> serde_json::Value {
    serde_json::json!({
        "api_url": option_env!("API_URL").unwrap_or("http://localhost:3000"),
    })
}

or

// src/config.rs
pub fn new() -> serde_json::Value {
    serde_json::json!({
        "api_url": env!("API_URL").expect("an API_URL is required to build"),
    })
}

At build time, you would provide the API URL during calls to make as follows for a test environment:

$ API_URL=https://api.test.yourapp.yourdomain make

or for a production environment:

$ API_URL=https://api.yourapp.yourdomain make

On application startup, adapters have their init() function invoked, which is where the config is passed in as a parameter.

Logging

When the Auric CLI generates your app skeleton with the init or new subcommand, the following default code is added into your src/main.rs which initializes the console_log crate:

fn main() {
    console_error_panic_hook::set_once();
    auric_runtime::console_log::init_with_level(log::Level::Debug)
        .expect("failed initializing logging");
    leptos::mount::mount_to_body(generated::views::application::ApplicationView);
}

So to log to the console from anywhere in your app, use console_log macros such as log::debug! or log::info! like this:

#[derive(AuricController, Default)]
pub struct MyController {}

#[async_trait(?Send)]
impl Controller for MyController {
    fn init(&mut self, _context: &dyn Context) -> Result<()> {
        let x = 5;
        log::debug!("MyController initialized with x={x}");
        Ok(())
    }
}

©2026 Megalithic LLC | Website | Blog | GitLab | Contact