Overview
Anansi is a simple full-stack web framework for Rust. Get started.
🛡️ Safety first
In addition to being written in Rust, Anansi provides defenses for common web security vulnerabilities.
⚙️ Performant
Anansi also allows web applications to run asynchronously with Rust’s speed (WIP).
✨ Easy to get started
Anansi handles many of the repetitive parts of web development, letting you work on the important parts of your app more quickly.
Records
Work with databases in Rust instead of SQL to write statically checked queries.
// A topic in a forum.
#[record]
#[derive(Relate, FromParams, ToUrl)]
pub struct Topic {
pub title: VarChar<200>,
pub user: ForeignKey<User>,
pub content: VarChar<40000>,
pub date: DateTime,
}
// A comment in a topic.
#[record]
#[derive(Relate, FromParams)]
pub struct Comment {
pub topic: ForeignKey<Topic>,
pub user: ForeignKey<User>,
pub content: VarChar<40000>,
pub date: DateTime,
}
Views
Mapping requests to views is simple.
pub fn routes<R: Request>() -> Router<R> {
Router::new()
.route("", TopicView::index)
.route("new", TopicView::new)
.route("load", TopicView::load)
.route("{topic_id}", TopicView::show)
}
#[viewer]
impl<R: Request> TopicView<R> {
// A view of the last 25 topics.
#[view(Site::is_visitor)]
pub async fn index(req: &mut R) -> Result<Response> {
let title = "Latest Topics";
let topics = Topic::order_by(date().desc())
.limit(25).query(req).await?;
let show_url = url!(req, Self::show);
let load_url = url!(req, Self::load);
}
}
Templates
Templates allow you to mix Rust with HTML for formatting.
@block title {@title}
@block content {
@load components {
<h1>@title</h1>
@if req.user().is_auth() {
@link req, Self::new {New Topic}
}
<ul>
@for topic in &topics {
<li>@link req, Self::show, topic {@topic.title}</li>
}
@if topics.len() == 25 {
<Loader @show_url @load_url />
}
</ul>
}
}
Components
Reactivity can be added with WebAssembly.
#[derive(Properties, Serialize, Deserialize)]
pub struct LoaderProps {
pub load_url: String,
pub show_url: String,
}
#[derive(Serialize, Deserialize)]
pub struct Data {
pub id: String,
pub title: String,
}
#[store]
#[derive(Serialize, Deserialize)]
pub struct Loader {
visible: bool,
page: u32,
fetched: Vec<Data>,
}
#[component(Loader)]
fn init(props: LoaderProps) -> Rsx {
let mut state = Self::store(true, 1, vec![]);
let (data_resource, handle_click) = resource!(Vec<Data>, state, props, {
*state.visible_mut() = false;
Request::get(&props.load_url)
.query([("page", state.page().to_string())])
});
rsx!(state, props, data_resource, {
@for data in state.fetched() {
<li>@href props.show_url, data.id {@data.title}</li>
}
@resource data_resource, state {
Resource::Pending => {
<Spinner />
}
Resource::Rejected(_) => {
*state.visible_mut() = true;
<div>Problem loading topics</div>
}
Resource::Resolved(mut f) => {
if f.len() == 25 && *state.page() < 3 {
*state.page_mut() += 1;
*state.visible_mut() = true;
}
state.fetched_mut().append(&mut f);
}
}
@if *state.visible() {
<button @onclick(handle_click)>Load more</button>
}
}
}
Scoped CSS
CSS can be isolated to individual components.
#[function_component(Spinner)]
fn init() -> Rsx {
style! {
div {
display: inline-block;
width: 25px;
height: 25px;
border: 3px solid #cfd0d1;
border-radius: 50%;
border-top-color: #1c87c9;
animation: spin 1s ease-in-out infinite;
-webkit-animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
@-webkit-keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
}
rsx! {
<div></div>
}
}