Using Rust for Webdev as a Hobby Programmer
October 8, 2016
I’ve been a huge fan of Rust ever since I started using it (which was right before 1.0 as I didn’t like updating my code all the time to the latest syntax). At first I created a few silly applications, some games, tried my hand at libraries; but in the end I was still drawn back into Web Development. You have to know that I grew up with PHP and then Ruby for Web Development, so I went from CakePHP to Ruby on Rails, which was amazing! RoR had so many features out of the box that I used, it just worked at first. But then came the pitfalls of dynamic frameworks: indirection over indirection over indirection. Which I believe is a case of “If you make it possible, people will use it.” This indirection is what causes the hours upon hours of debugging spent on why when removing a part from A it breaks B.
Enter Rust. I have personally found few caveats that I disagree with that are not simply design choices (based on taste rather than engineering) or were my fault all along. A few others have talked about their experiences, so if you want a more nuanced approach you could check those out, the /r/rust subreddit is also an excellent place to look for. (Some I’ve read: here, here, or here). While they do adress a few shortcomings it is overall possible to work with it. (And most problems seem not intrinsic to the language, but due to immaturity or ignorance of existing features/solutions).
A few months back I found the page arewewebyet.org and wanted to see how fun Rust would be to use. I will outline the different things I’ve learned and encountered here. If you think I could have gone into more detail or might have missed out on something please do tell me! (I can be reached on Reddit as TheNeikos or by mail at “neikos at neikos.email”)
Rust and the libraries
Due to the libraries and plugins I planned to use I had to switch to the nightly
branch of Rust. This can be a deal breaker to some, however I had no troubles so
far since the only thing unstable I use are plugin
, custom_derive
,
custom_attribute
and question_mark
. It might be that some of the crates use
more experimental features, however they are well-tested from what I saw.
So, after having run cargo init
I needed a few things that I was promised that
are covered:
- Web Framework
- ORM
- Crypto
- Logging
- Templating
What I wanted to write is a website to host an art community on. So I would also need an image handling library.
My choices so far are (with version numbers):
iron
(v0.4) – I like the middleware approach, I’m slowly getting to it’s pitfalls (regarding code duplication and resource loading) but I have ideas to solve those and thus still recommend using itdiesel
(v0.7) – A good looking (API-wise) library, while slightly verbose at times, it works really well.bcrypt
(v0.1) – I only need it for password hashing, so this is a weakly held choice in term of library. It works and it does what it says on the tin.log
(v0.3) +log4rs
(v0.4) – I had some log4j at University and didn’t like it (might be the Java though… brr) but log4rs seems to fit better into my paradigms and it works.maud
(v0.11) – My favorite find in terms of libraries. It allows you to write a Rust Macro that spits out HTML! It’s a first taste of what plugins will allow you to do and I love it.image
(v0.10) – An image library built for games, but it also works for webdev in this case: simple image manipulation.
A lot of these choices are subjective at the end due to the fact that there are many different choices and that none have differentiated themselves yet. So feel free to use something else! I can say for the aformentioned Libraries though that they do work together.
Middleware and Iron
Iron uses Handlers to respond to a given request, the way it is built allows for
quite a flexible array of patterns to be used. For example the one I use is a
mix of Router
and Chain
. Router
allows me to specify (in text) a mapping
from paths to another Handler. Chain
then allows me to run through a chain
of BeforeMiddleware
to AroundMiddleware
or AfterMiddleware
with my handler
in the middle. A few examples:
Note that this code is just to get an idea of what is possible, it might not compile due to missing implementations that would be too long/verbose to add!
Hello World in Iron
use iron::{Iron, Request, Response, IronResult};
fn handler(_: &mut Request) -> IronResult<Response> {
Ok(Response::with("Hello World"))
}
fn main() {
let server = Iron::new(handler);
match server.http("0.0.0.0:3000") {
Ok(()) => { /*Listening blocks the thread */ }
Err(e) => {
println!("Could not create Iron Server: {}", e);
}
}
}
Basic Router
extern crate iron;
extern crate router;
use iron::{Iron, Request, Response, IronResult};
use router::Router;
mod handlers {
use iron::{Request, Response, IronResult};
use router::Params;
pub fn world(_: &mut Request) -> IronResult<Response> {
Ok(Response::with("Hello World"))
}
pub fn answer(_: &mut Request) -> IronResult<Response> {
use params::{Params, Value};
let map = req.get_ref::<Params>().unwrap();
let name;
if let Some(&Value::String(ref nam)) = map.find("name") {
name = nam;
} else {
name = "Anonymous";
}
Ok(Response::with(format!("Hello {}", name)))
}
}
fn main() {
let mut router = Router::new();
router.get("/", handlers::world, "index");
router.get("/hello/:name", handlers::answer, "answer");
let server = Iron::new(router);
match server.http("0.0.0.0:3000") {
Ok(()) => { /*Listening blocks the thread */ }
Err(e) => {
println!("Could not create Iron Server: {}", e);
}
}
}
More Advanced Routing with Middleware and Mount
extern crate iron;
extern crate router;
extern crate mount;
use iron::{Iron, Request, Response, IronResult, Chain};
use router::Router;
use mount::Mount;
mod controllers;
fn main() {
let user_router = router! {
user_index: get "/" => controllers::user::index,
user_view: get "/:id" => controllers::user::view,
user_create: post "/" => controllers::user::create,
user_update: post "/:id" => {
let mut chain = Chain::new(controllers::user::update);
chain.link_before(Authorizer::new(
IsOwnerOf::<User>::new()
));
chain
},
};
let mut mount = Mount::new();
mount.mount("/user", user_router);
let server = Iron::new(mount);
match server.http("0.0.0.0:3000") {
Ok(()) => { /*Listening blocks the thread */ }
Err(e) => {
println!("Could not create Iron Server: {}", e);
}
}
}
For an example of how I am currently using this you can check out my routing as well as my authorization implementation.
All in all I find that other than the verbosity of using router::Param
it is
quite enjoyable and flexible to use.
The ORM, Diesel (with r2d2)
Diesel is a quite ergonomic compiler plugin that allows one to specify Rust structs for given database tables. I’ve only used it in conjunction with Postgres so I cannot say anything about other backends.
The Basic Premise
There are a few steps to get Diesel up and running to be able to use it nicely:
- Create a module that calls
infer_schema!(<DATABASE_URL>)
to generate Rust code for your database - Create a Struct to be used as a representation of a given table (examples below)
- Use
rd2d
andr2d2_diesel
to create a connection pool to your Database - Start using it
It is actually really simple to use once one gets beyond the setup phase (which at the time I was starting out was not clear at all).
The way I use it is to create a lazy_static
connection pool one can clone:
use std::env;
use diesel::pg::PgConnection;
use r2d2_diesel::ConnectionManager;
use r2d2;
lazy_static! {
static ref CONNECTION: r2d2::Pool<ConnectionManager<PgConnection>> = {
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
let config = r2d2::Config::default();
let manager = ConnectionManager::<PgConnection>::new(database_url);
r2d2::Pool::new(config, manager).expect("Failed to create pool")
};
}
pub fn connection() -> r2d2::Pool<ConnectionManager<PgConnection>> {
CONNECTION.clone()
}
then in another module I infer the schema:
infer_schema!(dotenv!("DATABASE_URL"));
Then, I define the actual model:
#[derive(Queryable, Identifiable, Debug)]
pub struct User {
pub id: i64,
pub email: String,
pub password_hash: String,
pub name: String,
pub created_at: diesel::data_types::PgTimestamp,
pub updated_at: diesel::data_types::PgTimestamp,
profile_image: Option<i64>
}
It is important that the fields are in the same order as you have defined them in your Database. This is an implementation problem that might be fixed in the Future so I’ve read, If you do not it will not compile.
Then, you can start using it:
pub fn find(uid: i64) -> Result<Option<User>, error::Error> {
use diesel::prelude::*;
use models::schema::users::dsl::*; // This import allows you to use the
// table names as a dsl
// It has the same name as your DB table
users.limit(1).filter(id.eq(uid))
.get_result::<models::user::User>(&*database::connection().get().unwrap())
.optional()
}
Note: The error::Error
type here is a custom one
What I find nice is that you need to create a second struct to be able to insert:
#[insertable_into(users)]
pub struct NewUser<'a> {
pub email: &'a str,
pub password_hash: String,
pub name: &'a str,
}
And then:
pub fn create_from(nu: NewUser) -> Result<i64, error::FurratoriaError> {
use diesel;
use diesel::prelude::*;
use models::schema::users::dsl::*;
let user_id = try!(diesel::insert(&nu)
.into(users)
.returning(id)
.get_result(&*database::connection().get().unwrap())
);
Ok(user_id)
}
For a complete overview of how one could handle users you can check out how I do it.
Using Maud for Templates
In the end I wanted to output HTML, since format!
ing everything seemed like a
bad idea. In the end I went with Maud since I liked the idea of working with a
plugin.
It is also quite intuitive to work with:
let name = "Noone";
let output = html! {
div.hello "World!"
p {
strong { "Hello " (name) }
}
};
It returns a RenderOnce<String>
which means it is easy to use and reason with.
The thing I found really fancy is that you can nest those calls! Allowing to me
to do something like that:
html! {
div.row (PreEscaped(Column::new(html! {
h1 "Hello there!"
p {
"Heya there"
}
})))
};
Which then in turn allows me transform or wrap the code!
Another neat thing is to build forms:
html! {
(PreEscaped(Form::new(FormMethod::Post, &format!("/submissions/{}", sub.id))
.with_encoding("multipart/form-data")
.with_fields(&[
&Input::new("Image", "sub_image")
.with_type("file")
.with_errors(errors.as_ref().map(|x| &x.image)),
&Input::new("Title", "sub_name")
.with_value(&sub.title[..])
.with_errors(errors.as_ref().map(|x| &x.title)),
&Textarea::new("Description", "sub_desc")
.with_value(&sub.description)
.with_errors(None),
&Select::new("Visibility", "sub_visibility")
.add_option("Public","0")
.add_option("Private", "2")
.with_selected(sub.get_visibility().as_str()),
&Input::new("", "")
.with_value("Update")
.with_type("submit")
.with_class("btn btn-primary")
])))
}
Although Form
and Column
are my own types I built.
Conclusion
All in all I think that it is quite possible to write Web Applications nicely. While one has to use Rust nightly with my choice of libraries one could also use diesel with syntex on the stable branch and substitute Maud with something like Horrorshow.