tokamak icon indicating copy to clipboard operation
tokamak copied to clipboard

Friendliest http framework, made for Dioxus (under construction 🏗)

Tokamak

Rust's friendliest http framework

Tokamak is a simple web-framework for Rust/Tokio focused on maximum developer productivity.

Features

  • Thread-per-core architecture for simple async
  • Prebuilt components for auth, admin, cookies, jwt, sessions, sitemaps, and more
  • Dioxus LiveView for server-rendered apps
  • Progressively rendered static pages
  • Prebuilt admin panel and logging
  • Hyper and Tower integrations (like Axum)

Tokamak combines the batteries-included philosophy of django with the composability of express. Bring your own parts, or compose our prebuilt components to fit your app.

A snippet

Tokamak is incredibly simple but incredibly flexible.

#[tokio::main]
async fn main() {
    let mut app = tokamak::new();

    app.at("/").get(|_| "hello world!");

    app.listen("0.0.0.0:8080").await;
}

Filters

In Tokamak, you can easily filter endpoints based on criteria. Tokamak prefers functions over traits, and all filters are just functions. This saves you from thinking about lifetimes, async traits, pinning, boxing, or anything else that might seem too advanced.

 app.at("/")
    .filter(|req| req.header_exact("auth-bearer"))
    .get(|_| "hello world!");

Extractors

Extractors provide a way of guarding an endpoint and returning a value.

 app.at("/")
    .extract(|req, state| state.authorize(req))
    .get(|req, state, user: Admin| "hello world!");

In the cases where you need an extraction just for a single handler, it can be easily done from within the handler itself.

fn admin_panel(req: Request, state: &State) -> Response {
  let admin = state.authorize::<Admin>(req)?;
}

Both of these strategies are "explicit" extractors. However, some extractors can be made implicit through the FromRequest trait.

fn admin_panel(req: Request, state: &State, admin: Admin) -> Response {
  todo!()
}

struct Admin {
  id: Uuid
}

impl FromRequest for Admin {
  fn parse(req: Request) -> TokamakResult<Self> {
    let auth = req.context::<AuthEngine>()?
    let token = req.cookie("app-auth")?;
    auth.is_authorized(token)
  }
}

They then can be easily composed in your app as a quick fallback strategy:

app.with(AuthEngine::new());

app.at("/api")
    .get(|req, state, admin: Admin| Ok("You are an admin!"))
    .get(|_| "You are not an admin");

Middleware

Again, like filters, extractors, and endpoints, Tokamak's middleware is just another function. No traits!

app.at("/")
    .with(|req, state, res| res.insert_header("request-number", format!(state.count_up())))
    .get(|_| "hello world!");

Of course, we can refactor out our middleware into dedicated functions

app.at("/")
    .with(counting_middleware)
    .get(|_| "hello world!");

Tower Layers

Because Tokamak uses hyper under the hood, you are also free to add Tower layers into your app:

app.at("/")
    .layer(identity_layer)
    .get(|_| "hello world!");

Context

Middleware have the ability to add their own state into your app. This is provided as "context":

let mut app = tokamak::new();

app.with(AuthEngine::new());

app.at("/").get(|req| {
  let engine = req.context::<AuthEngine>()?;
})

This also means an ecosystem of layers, middleware,

Paths, params, queries, and uri

Everything you might care about for an endpoint is accessible on the request directly.

fn load_app(req: Request) -> Response {
    let id = req.param("id")?;
    let name = req.param("name")?;
    let body = req.body_json::<Dog>()?;

    Ok(format!("Hello, {name}, your request ID is {id}").into())
}

Magical Macros

If you like macros, then we've got them. Tokamak's macros essentially take care of the parsing steps that you'd write on your own. No magic, just cleaning up boilerplate:

#[get("/app/:id/:name")]
fn load_app(req: Request, id: i32, name: String, body_json: Dog) -> Response {
  Ok(format!("Hello, {name}, your request ID is {id}").into())
}

Forms

Websockets, SSE

Tokamak is built for websockets and server-side-events. WebSocket handlers are !Send, so you can freely use Cell/Rc, etc, etc.

app.at("app").ws(|req, state, (rx, tx)| {
  loop {
    let msg = rx.next().await?;
    tx.send(b"hello!")?;
  }
});
app.at("app").sse(|req, state, (rx, tx)| {
  loop {
    let msg = rx.next().await?;
    tx.send(b"hello!")?;
  }
});

Immediate returns

Lots of endpoints don't need async. However, most Rust HTTP frameworks end up pin/boxing these anways. Tokamak knows the difference between a sync and an async handler and will immediately return any value if possible. No heap allocation required!

!Send handlers and blocking handlers.

Internally, Tokamak uses a LocalPool to manage threads to spawn handle requests. This means all handlers are !Send.

However, you can tweak an endpoint or Router be completely blocking (allowing syncronous IO) or disable !Send for theoretical performance improvements.

// Blocking
app.blocking_at("app").get(|_| block_some_io());

// work stealing
app.work_stealing_at("app").get(|_| "asd");

Remaining topics:

  • Nested
  • Sessions
  • Cookies