Fork me on GitHub

Thruster

A fast, middleware based, web framework written in Rust

Quick Start

The easiest way to start is to use the thruster-cli. To install, simply run:

cargo install thruster-cli
    

This will install the cli on your computer, so now you can access it via thruster-cli. Once you have that installed, let's start by making a new project:

thruster-cli init my-awesome-project
cd my-awesome-project
docker-compose up postgres -d
    

The default thruster project is created with postgres as a backing db as well as a docker setup. The above comands just start postgres in a container, as compiling rust in a docker container can be a bit slow, but the default setup will also run fully in containers if you wish. You'll also notice that all of the code is generated and shown to you as the developer. One of the core tenants of the cli is to not hide code behind "magic."

Why don't we make a quick blog! Starting with a "post." To generate a post object, all we have to do is:

thruster-cli component post
    

follow the instructions from the cli to wire up the route.

Now you'll see that we have a new model, along with accompanying service and controller. Thruster apps should always be laid out like this; the controller handles actual requests and responses, while an underlying service is a framework agnostic layer that manages the data. If data persistence is required, then a model layer can lie beneath that.

The default model only has an id and a test field, so let's update that. Change src/models/posts.rs like so:

use crate::schema::posts;

#[derive(Debug, Deserialize, Serialize, Queryable)]
pub struct Post {
  pub id: i32,
  pub title: String,
  pub content: String
}

#[derive(Insertable, Debug, Deserialize, Serialize)]
#[table_name="posts"]
pub struct NewPost<'a> {
  pub title: &'a str,
  pub content: &'a str
}
    

This updates our model to have a "title" and "content" fields, which are just strings. We also need to make a migration, update migrations/_create_post/up.sql.

CREATE TABLE posts (
  id         SERIAL  PRIMARY KEY,
  title      TEXT    NOT NULL,
  content    TEXT    NOT NULL
);
    

Now, we need to run our new sql migrations, and generate our schema (for diesel.) The schema is generated directly from the currently running postgres instance.

thruster-cli migrate
    

You can generate a few new posts now using curl, like this!

curl -X POST http://localhost:4321/posts \
  -H "Content-Type: application/json" \
  -d '{"title":"My first post!", "content":"I guess this is a really short first post."}'
    

Posts aren't much good without a way to read them! Let's add some server side rendering. First things first, let's add a templating engine. You can really use anything you'd like, but I really like the feel of askama. If you're using askama, add askama = "0.7.2" to your Cargo.toml file. Now we'll need to add a templates folder. In which, we'll add a file called post.html and add the following:

<html>
  <body>
    <h1>{{ title }}</h1>
    <p>{{ content }}</p>
  </body>
</html>
    

The first thing we're going to do is create a controller. We won't need a full controller-service-model combo, so let's make a new blank file, src/pages/pages_controller.rs

use crate::context::{ Ctx };
use thruster::{MiddlewareChain, MiddlewareReturnValue};

use crate::posts::post_service;
use crate::models::posts::{ Post };
use futures::future;
use std::boxed::Box;
use askama::Template;

#[derive(Template)]
#[template(path = "post.html")]
struct PostTemplate<'a> {
  title: &'a str,
  content: &'a str
}

pub fn get_post(mut context: Ctx, _next: impl Fn(Ctx) -> MiddlewareReturnValue  + Send + Sync) -> MiddlewareReturnValue {
  fn error(mut context: Ctx) -> MiddlewareReturnValue {
    context.status(400);
    context.body("Could not get Post");
    Box::new(future::ok(context))
  }

  let id = match context.params.get("id") {
    Some(_id) => _id,
    None => return error(context)
  };

  let id_as_number = match id.parse::() {
    Ok(_id_as_number) => _id_as_number,
    Err(_) => return error(context)
  };

  let fetched_result = match post_service::get_post(id_as_number) {
    Ok(_fetched_result) => _fetched_result,
    Err(_) => return error(context)
  };

  let template = PostTemplate {
    title: &fetched_result.title,
    content: &fetched_result.content
  };

  context.body(&template.render().unwrap());

  Box::new(future::ok(context))
}
    

The first part, fn error... is a convenience method for generating an error response. Then, we fetch the "id" from the route parameters of the request. Then, we have to parse that parameter into an integer. We then fetch the post using the newly parsed id, and finally we render a the template with the recently fetched post data.

We have to also add this to the src/pages/mod.rs file. This serves as the route linking for the controller:

mod pages_controller;

use thruster::{App, middleware, MiddlewareChain, MiddlewareReturnValue, Request};
use crate::context::{generate_context, Ctx};
use crate::pages::pages_controller::{
  get_post
};

pub fn init() -> App {
  let mut subroute = App::::create(generate_context);

  subroute.get("/:id", middleware![Ctx => get_post]);

  subroute
}
    

And finally, we add this to the main file, we need to add mod pages; as well as use crate::pages::{init as page_routes};. In order to actually call the route, we add this:

app.use_sub_app("/pages", page_routes());
    

Now, assuming you've created a post using curl, you can visit http://localhost:4321/pages/1 and see your very first post!