Like many of us, I'm quite lazy. When making a wep application, lots of the core functionality will be the same from codebase to codebase. You need to respond to HTTP requests, generate and return HTML bodies, serve static assets, handle unknown routes. There's no pressing need to reinvent all of this from the ground up just to render a new webpage.
This is why we have frameworks, we don't like doing this stuff over and over again.
However, also like many of us, I'm quite particular. This XKCD comes to mind a lot:
Most CLI scaffolding tools give me this feeling. I get that all the extra boilerplate is actually stuff I want, but I don't know what it all is.
So, I made my own.
If you're like me, you won't use this or any other template. However, if you ARE me, you will, because you built it! Otherwise, it may be helpful for doing your own.
Here's the GitHub repo. You can click the handy "Use this template" button and get going.
Here's the highlights:
- Hyper - No framework, just a small and fast HTTP server library.
- Askama - Typesafe, compiled templates.
- TailwindCSS - Granular styling for people who don't know how to use actual CSS.
- Docker - Simple, quick deployment.
- Github Action - Get a fancy green check mark next to your commits.
Let's take a quick tour.
$ tree
.
├── Cargo.lock # Rust package lockfile
├── Cargo.toml # Rust package metadata
├── Dockerfile # Docker container build instructions
├── LICENSE # I use the BSD-3-Clause License
├── README.md # Markdown readme file
├── package.json # NPM package metadata
├── pnpm-lock.yaml # NPM package lockfile
├── postcss.config.js # CSS processing configuration
├── src
│ ├── assets
│ │ ├── config.toml # Set runtime options (port, address)
│ │ ├── images
│ │ │ └── favicon.ico # Any static images can live here
│ │ ├── main.css # Postcss-compiled styelsheet - don't edit this one
│ │ ├── manifest.json # WebExtension API metadata file
│ │ └── robots.txt # Robots exclusion protocol
│ ├── config.rs # Read config.toml/CLI options, init logging
│ ├── css
│ │ └── app.css # App-wide stylesheet - DO edit this one
│ ├── handlers.rs # Rust functions from Requests to Responses
│ ├── main.rs # Entry point
│ ├── router.rs # Select proper handler from request URI
│ ├── templates.rs # Type information for templates
│ └── types.rs # Blank - use how you like!
├── stylelintrc.json # CSS style lint options
└── templates
├── 404.html # Not Found stub
├── index.html # Main page template
└── skel.html # App-wide skeleton markup
5 directories, 24 files
One of the oddities (and cool things) is that the assets/
directory actually lives inside src/
. This is because all of these text file assets are included right in the binary as static strings via the include_str!()
macro. When you deploy, none of this extra stuff is present. The deployment directory will look like this, if Docker is not used:
$ tree
.
├── LICENSE # I use the BSD-3-Clause License
├── README.md # Markdown readme file
├── images
│ └── favicon.ico # Favicon
└── hyper-template # Executable
Just run the thing!
I'll briefly unpack a few of these files. Let's look at main.rs
first:
#[tokio::main]
async fn main() {
init_logging(2); // set INFO level
let addr = format!("{}:{}", OPT.address, OPT.port)
.parse()
.expect("Should parse net::SocketAddr");
let make_svc = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(router)) });
let server = Server::bind(&addr).serve(make_svc);
info!("Serving {} on {}", env!("CARGO_PKG_NAME"), addr);
if let Err(e) = server.await {
eprintln!("Server error: {}", e);
}
}
The only part of this file you might touch is in the make_service_fn
call. This stub assumes your handlers cannot fail and uses std::convert::Infallible
. This means that any errors that do pop up in this call (so, your router and handlers) will need to be handled right there, with unwrap()
or expect()
. You can get yourself a little more flexibility by simply swapping in anyhow::Error
! That way, all those unwrap()
s can turn into ?
s. This is what I've done personally when using this template, but I decided not to make that choice for you - that felt like an overreach in a minimal template.
Also, notably, Rust has async/await
now! This is very cool syntax for some features (Futures) that have already existed, making the whole thing much much more accessible. No more crazy 120-char types! For a primer, start here.
You won't really need to touch this. It just sets up the asynchoronous runtime and converts your actual application to a state machine that can use it. In this case, our actual application is the function router()
. That looks like this:
pub async fn router(req: Request<Body>) -> HandlerResult {
let (method, path) = (req.method(), req.uri().path());
info!("{} {}", method, path);
match (method, path) {
(&Method::GET, "/") | (&Method::GET, "/index.html") => index().await,
(&Method::GET, "/main.css") => {
string_handler(include_str!("assets/main.css"), "text/css", None).await
}
(&Method::GET, "/manifest.json") => {
string_handler(include_str!("assets/manifest.json"), "text/json", None).await
}
(&Method::GET, "/robots.txt") => {
string_handler(include_str!("assets/robots.txt"), "text", None).await
}
(&Method::GET, path_str) => {
// Otherwise...
// is it an image?
if let Some(ext) = path_str.split('.').nth(1) {
match ext {
"ico" | "svg" => image(path).await,
_ => four_oh_four().await,
}
} else {
four_oh_four().await
}
}
_ => {
warn!("{}: 404!", path);
four_oh_four().await
}
}
}
All of the handlers eventually pass through to this function:
/// Top-level handler that DEFLATE compresses and responds with from a &[u8] body
/// If None passed to status, 200 OK will be returned
pub async fn bytes_handler(
body: &[u8],
content_type: &str,
status: Option<StatusCode>,
) -> HandlerResult {
// Compress
let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
e.write_all(body).unwrap();
let compressed = e.finish().unwrap();
// Return response
Ok(Response::builder()
.status(status.unwrap_or_default())
.header(header::CONTENT_TYPE, content_type)
.header(header::CONTENT_ENCODING, "deflate")
.body(Body::from(compressed))
.unwrap())
}
It takes your response body as a byte slice and compresses it before returning it, adding the proper headers. Lots of resources are going to be HTML, but we're always using this anyway:
pub async fn string_handler(
body: &str,
content_type: &str,
status: Option<StatusCode>,
) -> HandlerResult {
bytes_handler(body.as_bytes(), content_type, status).await
}
pub async fn html_str_handler(body: &str) -> HandlerResult {
string_handler(body, "text/html", None).await
}
These templates all get a specific struct:
use askama::Template;
#[derive(Default, Template)]
#[template(path = "skel.html")]
pub struct SkelTemplate {}
#[derive(Default, Template)]
#[template(path = "404.html")]
pub struct FourOhFourTemplate {}
#[derive(Default, Template)]
#[template(path = "index.html")]
pub struct IndexTemplate {}
They're blank for now, there's no data flowing through. If you wanted to pass a string into the index, it might look like this:
#[derive(Default, Template)]
#[template(path = "index.html")]
pub struct IndexTemplate<'a> {
pub name: &'a str,
}
Now you need to instantiate the struct with properly typed data, and you can use {{ name }}
inside your template file.
This template comes pre-hooked up with Tailwind - here's app.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
Again, it's your app, not mine. When you're ready to style, just start right below these directives - or directly in your templates! The provided NPM scripts will compile all your CSS into src/assets/main.css
before compiling the Rust binary, so it too can be included as a static string.
That's pretty much it! This app is extremely barebones, just how I like my templates. I just successfully used this template to spin up a more complicated application, with a database and some scraping logic, and starting from here instead of from scratch saved me a few hours at the beginning. YMMV.
Stay tuned for two less-minimal variants of this - one for building a static blog, and one with a database and ORM hooked up!
There is definitely room for improvement, here. The code could be refactored (middleware, maybe?), there needs to be tests, etc. I'll get there eventually, but also, I'll take a PR :)
Photo by Shiro hatori on Unsplash
Top comments (10)
Great stuff - but Ben... where are the tests? :D
Actually, I'm not just trolling you. I've been dipping my toes back into Rust for a few days and started trying to write something in Hyper. I tried to do what I would normally do for this sort of thing: write a test for one of the handler functions.
I got to this:
Which I'm not crazy about - feels like I'm repeatedly unwinding a future then unwrapping a result. Gets the job done but feels a bit like I'm banging these rocks together. You know anything more elegant?
Heh - you nailed it with banging rocks together. I'm still playing with these, I don't know what to do either:
That made me feel better - misery loves company!
I'm planning to include a fuller set with the forthcoming CRUD template, I'll copy the relevant ones back over.
They may not be pretty, but at least they'll be there...
Great post, you always seem to post just what I'm looking for, are you reading my mind?, be honest, are you?, are you doing it now?
I'm in the look for an alternative to Actix for a plan B or maybe migrate from it entirely, and so far Tide, Warp and plain Hyper looks like my contenders, did you check Tide out? what made you take Hyper instead of their own Warp?
I've not actually used Tide or Warp, but they do both look cool. Honestly, I'm still lurched by breaking API changes and an unstable ecosystem. I stuck with Hyper because it's simple, and while it does still change (recently, to support async/await), it likely won't change much further at this point. All these other tools are built around it, I believe.
I think I need to wait for more complex tools to bake longer before I'm ready to start investing time in things like personal use templates. I want this and the other Hyper templates I produce to kind of be "set it and forget it" to the extent possible, and Tide just ain't it yet.
I also already knew how to use it from before the syntax change. I have no complaints about it, which is reason enough to not tool-hop!
Wonderful.
I think to work professional, I have not to use any frameworks for security and also business safe.
I would prefer to build web server using the rust book example rather than using any open source framework.
what is your opinion?
you know what happen with actix then now where is rockets.
how can I assure that the frame wrok does not have any spam.
Do not tell me to check the code because if I go to framework to save time so that will be no sense to study the code
Agreed - though honestly, I wish I COULD use Rocket. It actually looks great, but nightly-only is a complete dealbreaker.
As a huge fan of microframeworks with swappable plug-ins I approve of this 🔥🔥🔥
Still gotta learn Rust though 😂
Hey, me too!