Vial
~ a micro micro-framework ~
Vial is a small web framework for making small web “sites” in Rust. It includes just a handful of basic features for delivering old school, server-side rendered HTML: request routing, form data parsing, response building, and serving static file assets.
The goal is a small, lean core that compiles quickly and has as few dependencies as possible. Use it for HTML stuff: prototyping ideas, testing out concepts, or, perhaps, even writing tiny personal apps. Nothing serious though, got it?
This manual is an overview of Vial’s built-in features, as well as the few optional features you can enable. It also includes suggestions for some “common tasks”, like using a database to store information.
Hello World
Here’s the bare minimum:
vial::routes! {
"/" => |_| "Greetings, creature.";
GET }
fn main() {
vial::run!();
}
That should tell you a lot, in that there isn’t a lot to Vial.
Now here’s a bigger bear, showing off more of Vial’s features:
use vial::prelude::*;
routes! {
"/" => hello_world;
GET "/" => redirect_to_greeting;
POST "/:name" => hello_name;
GET "/*path" => |req|
GET Response::from(404).with_body(
format!("<h1>404 Not Found: {}</h1>",
.arg("path").unwrap_or("")));
req}
fn hello_world(_req: Request) -> &'static str {
"<h1>Hello, world!</h1>
<p><strong>What's your name?</strong></p>
<form method='POST' action='/'>
<p><input name='name' type='text'/></p>
<p><input type='submit'/></p>
</form>"
}
fn redirect_to_greeting(req: Request) -> Option<impl Responder> {
let name = req.form("name")?;
Some(Response::redirect_to(format!("/{}", name)))
}
fn hello_name(req: Request) -> String {
format!(
"<h1>Why hello there, {}!</h1>",
.arg("name").unwrap()
req
)}
fn main() {
run!().unwrap();
}
You can run the above example from the root of this repository:
$ cargo run --example manual
Vial comes with a handful of examples in the
examples/
directory, so be sure to peruse them skeptically
- either alongside or after digesting this manual.
Overview
Like most web library thingy-jingies that only focus on server-side rendering, there are three main parts to a Vial application:
Routing: You write actions that take a Request and return either a Response or a Responder, then map them to URLs and URL patterns using the vial::routes! macro.
Requests: The Request object provides information about each client’s humble request.
Responses: Your actions return either a Response struct, which can be easily built, or a type that implements the Responder trait, like
String
orOption<Response>
.
Getting Started
Vial should work on any recent, stable version of Rust on Linux or macOS.
To begin, add Vial to your project’s
Cargo.toml
:
[dependencies]= "0.1" vial
Now all you have to do is call vial::routes!
to define
your routes and vial::run!
to start the server in
src/main.rs
:
vial::routes! {
"/" => |_| "It works!";
GET }
fn main() {
vial::run!();
}
This should start a server at http://0.0.0.0:7667 and tell you that it did. Congratulations! You’re on your way.
Routing
Routing is the real gravy and potatoes of any web framework, if you think about it. In Vial, routes are defined with the vial::routes! macro in this format:
HTTP_METHOD ROUTE_PATTERN => ACTION;
The order in which routes are written matters - routes written first
will be checked for matches first, meaning you can declare many routes
that point to "/"
, but only the first one defined will ever
match.
HTTP Methods
HTTP_METHOD
can be one of:
GET
HEAD
POST
PUT
DELETE
PATCH
Route Patterns
ROUTE_PATTERN
can be an exact match, such as
"/user"
or "/v2/search.php3"
, or it can
include a named parameter:
"/:name"
— This will match anything except paths with/
or.
in them."/:name.md"
— Use this format to match on a specific file extension."/*name"
— This will match everything, including/
and.
In the three examples above, calling request.arg("name")
in an Action will return Some(&str)
.
Note that you can have multiple parameters in the same route, as long as the “match all” pattern occurs last:
vial::routes! {
"/:category/:id/*name" => |req| format!(
GET "<p>Category: {}</p>
<p>ID: {}</p>
<p>Name: {}</p>",
.arg("category").unwrap_or("None"),
req.arg("id").unwrap_or("None"),
req.arg("name").unwrap_or("None"),
req;
)}
fn main() {
vial::run!();
}
Actions
Actions are what routes actually route to. They are functions or closures take a Request and return either a Response or something that implements the Responder trait:
use vial::prelude::*;
routes! {
"/info" => |req| format!(
GET "<p>Name: {}</p>", req.query("name").unwrap_or("None")
;
)"/" => index;
GET }
fn index(req: Request) -> &'static str {
"<form method='GET'>
<p>Enter your name: <input type='text' name='name'/></p>
<input type='submit'/>
</form>"
}
fn main() {
run!();
}
Returning impl Responder
is easy - Responder is a Vial trait that
defines a single conversion method that returns a Response:
pub trait Responder {
fn to_response(self) -> Response;
}
These types implement Responder
by default:
&str
String
usize
- Empty response with this number as the status code.Option<impl Responder>
- 404 onNone
Result<impl Responder, Error>
- 500 on Error
Filters
Filters are functions that are run before actions. They can either
modify the existing request before it’s sent to your action, or they can
return a Response
that will be delivered to the client
without any actions being called:
fn(req: &mut Request) -> Option<Response>;
Like Rust’s attributes, they can either apply to all routes defined
in the same vial::routes!
macro call or just a specific
route:
use std::sync::atomic::{AtomicUsize, Ordering};
use vial::prelude::*;
routes! {
// `count` will run before all routes in this block
#![filter(count)]
"/" => |_| "Hey there!";
GET "/hits" => hits;
GET
// `count` will run again when /double is visited
#[filter(count)]
"/double" => double;
GET
// `echo` will be called when /echo is visited
#[filter(echo)]
"/echo" => |_| "Is there an echo in here?";
GET }
fn hits(req: Request) -> impl Responder {
format!("Hits: {}", req.counter().count())
}
fn double(req: Request) -> impl Responder {
"Double trouble."
}
fn echo(req: &mut Request) -> Option<Response> {
println!("{:#?}", req);
None
}
fn count(req: &mut Request) -> Option<Response> {
.counter().incr();
reqNone
}
#[derive(Debug, Default)]
struct Counter(AtomicUsize);
impl Counter {
fn count(&self) -> String {
self.0.load(Ordering::Relaxed).to_string()
}
fn incr(&self) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
trait WithCounter {
fn counter(&self) -> &Counter;
}
impl WithCounter for Request {
fn counter(&self) -> &Counter {
self.state::<Counter>()
}
}
fn main() {
use_state!(Counter::default());
run!().unwrap();
}
Route Modules
Routes can be defined in different modules and combined together with
vial::run!
:
mod blog;
mod wiki {
vial::routes! {
"/wiki" => |_| "This is the wiki.";
GET }
}
vial::routes! {
"/" => |_| "Index page.";
GET }
fn main() {
vial::run!(self, blog, wiki);
}
Requests
When a route matches and an Action is called, it’s passed a Request object. Request
contains
information about the request itself, as well as a number of helper
methods.
Route Parameters
As mentioned in the Routing section above, you
can define parameters in a route and access their value for a given
request using request.arg()
:
vial::routes! {
"/:animal" => |req| format!(
GET "Animal: {}", req.arg("animal").unwrap_or("None")
;
)}
Query Parameters
In addition to route parameters, Vial will also parse good ol’ fashioned query string parameters for you:
vial::routes! {
"/info" => |req| format!(
GET "Version: v{}",
.query("version").unwrap_or("?")
req;
)}
fn main() {
vial::run!();
}
Running this and visiting /info
will show:
Version: v?
But visiting /info?version=1.0
will show:
Version: v1.0
Like arg()
, query()
returns
Option<&str>
.
Form Data
What’s the web without open ended <textarea>s
?
Perish the thought.
POSTed form data follows the same pattern as query and route
parameters: use request.form()
to access a form
parameter:
use vial::prelude::*;
use db;
routes! {
"/show/:id" => show;
GET "/new" => new;
GET "/new" => create;
POST }
fn new(_req: Request) -> impl Responder {
"<form method='POST'>
<p>Name: <input type='text' name='name'/></p>
<p>Location: <input type='text' name='location'/></p>
<p><input type='submit'/></p>
</form>"
}
fn create(req: Request) -> Result<impl Responder, io::Error> {
let id = db::insert!(
"name" => req.form("name").unwrap(),
"location" => req.form("location").unwrap()
?;
)Ok(Response::redirect_to(format!("/show/{}", id)))
}
fn show(req: Request) -> Option<impl Responder> {
let record = db::query!("id" => id).ok()?;
format!(
"<p>Name: {}</p>
<p>Location: {}</p>",
.get("name").unwrap_or("None"),
record.get("location").unwrap_or("None"),
record
)}
fn main() {
run!();
}
Request Headers
Headers are available without any of the peksy conveniences of type
safety. Just give request.header()
a string and hope you
get one back!
use vial::prelude::*;
use std::{fs, path::Path};
routes! {
"/:file" => show;
GET }
fn show(req: Request) -> Option<impl Responder> {
let path = format!("./{}", req.arg("file")?);
if Path::new(&path).exists() {
if req.header("Accept").unwrap_or("?").starts_with("text/plain") {
Some(Response::from_header("Content-Type", "text/plain")
.with_file(&path))
} else {
let file = fs::read_to_string(&path).unwrap();
Some(Response::from_body(format!(
"<html><body>
<pre style='width:50%;margin:0 auto'>{}</pre>
</body></html>", file)))
}
} else {
None
}
}
fn main() {
run!();
}
Header names are case insensitive, though, so at least you don’t have to worry about that.
Other Info
Beyond the headers, Request
also surfaces a few more
basic bits of information such as the request.method()
and
request.path()
:
impl Request {
// "GET", "POST", etc. Always uppercase.
fn method(&self) -> &str;
// Always starts with "/"
fn path(&self) -> &str;
}
Responses
Every Action returns either a Response or a type that implements the Responder trait’s single method:
pub trait Responder {
fn to_response(self) -> Response;
}
Common types like &str
and
Option<String>
already implement this, so you are
free to be lazy and return simple types in your Actions. If, however,
you want to set headers and do other fancy jazz, you’ll need to build
and return a Response directly.
Rather than use the “Builder” pattern like more mature and better designed libraries, Vial’s Response lets you set properties either directly or using Builder-style methods:
vial::routes! {
"/404" => |_| Response::from(404).with_text("404 Not Found");
GET }
Each Response
defaults to a Content-Type
of
text/html; charset=utf8
, so you can build HTML with your
bare hands:
fn index(_req: Request) -> impl Responder {
Response::from("<marquee>Coming soon!</marquee>")
}
To produce plain text, set the header using
with_header()
or set_header()
, or use
with_text()
instead of with_body()
or
from_body()
:
fn readme(_req: Request) -> Response {
// This will be rendered as plain text.
Response::from_file("README.md")
.with_header("Content-Type", "text/plain")
}
Building Responses
The Response documentation contains more information on all the methods available, but here are some of the properties you can set on a Response in your actions:
fn from_text<S: AsRef<str>>(text: S) -> Response;
fn with_code(mut self, code: usize) -> Response;
fn with_body<S: AsRef<str>>(mut self, body: S) -> Response;
fn with_text<S: AsRef<str>>(self, text: S) -> Response;
fn with_json<T: serde::Serialize>(self, value: T) -> Response;
(requires thejson_serde
feature)fn with_json<T: nanoserde::SerJson>(self, value: T) -> Response;
(requires thejson_nano
feature)fn with_reader(mut self, reader: Box<dyn io::Read>) -> Response;
fn with_asset(mut self, path: &str) -> Response;
fn with_file(mut self, path: &str) -> Response;
fn with_error<E: error::Error>(self, err: E) -> Response;
fn with_header(mut self, key: &str, value: &str) -> Response;
Redirect
To issue a 302 redirect, use the redirect_to
static
method:
fn search(req: Request) -> Option<impl Responder> {
let url = format!(
"https://en.wikipedia.org/wiki/Special:Search?search={}",
.arg("search")?
req
)Some(Response::redirect_to(url))
}
Status Codes
Empty responses with status codes can be created from
usize
:
fn fourohno(_req: Request) -> impl Responder {
Response::from(404)
}
fn pay_me(_req: Request) -> impl Responder {
402
}
Headers
Headers can be set Builder-style using with_header
or
imperative-style using set_header
:
fn not_found(_req: Request) -> Response {
Response::from(404)
.with_header("Content-Type", "text/plain")
.with_body("404 Not Found")
}
fn download(req: Request) -> Option<impl Responder> {
Response::from_file(req.arg("file")?)
.with_header("Content-Type", "application/octet-stream")
}
fn perm_redirect(url: &str) -> Response {
Response::from(301).with_header("Location", url)
}
Serving Static Files
Vial can automatically serve static files out of an
asset directory, complete with proper ETag handling, if you tell it
which directory to use with the vial::asset_dir!
macro. It
can also optionally bundle those assets them into your application in
--release
mode, producing a single binary that was
developed as if it used separate CSS and JS files.
vial::asset_dir!
To get started, put all your .js
and .css
and other static assets into a directory in the root of your project,
such as assets/
. In your HTML, include those files as if
the “assets” directory were the root of your application. So if you have
“assets/app.js”, in your <script>
tag you would
include just “/app.js”.
Next call vial::asset_dir!()
with the path to your asset directory (maybe assets/
?)
before starting your application with vial::run!:
If we had a directory structure like this:
.
├── README.md
├── assets
│ └── img
│ ├── banker.png
│ └── doctor.png
└── src
└── main.rs
We could serve our images like so:
vial::routes! {
"/" => |_| "
GET <p><img src='/img/doctor.png'/></p>
<p><img src='/img/banker.png'/></p>
";
}
fn main() {
vial::asset_dir!("assets/");
vial::run!().unwrap();
}
asset::methods()
By setting an asset directory, either through the vial::asset_dir!()
or [vial::bundle_assets!()][bundle_assets api] macro, you can then use
the methods in the asset::
module to work with them:
asset::etag(): Get the ETag for an asset. Used automatically by the Router if a web request matches an asset’s path.
asset::exists(): Does an asset exist? Works regardless of whether the asset is bundled or not.
asset::is_bundled(): Are assets bundled? Only true in
--release
mode and when used with thevial::bundle_assets!()
macro.asset::to_string(): Like
fs::read_to_string()
, delivers the content of an asset as aString
.asset::as_reader(): Like
asset::to_string()
but provides anio::Read
of an asset, whether or not it’s bundled.
Bundling Assets
Vial is meant to be small and swift, like a ninja
star. Part of that means Vial apps should be able to
compile into standalone binaries that don’t rely on the
filesystem. In order to accomplish this, Vial can
bundle your assets into your final --release
binary for
you. As long as you the asset::()
API described above to
access them, all your code will work the same whether your assets are
bundled or not.
Combined with the ETag support, this means all assets will be reloaded by your browser whenever they’re modified in dev mode for a smoother developmental experience.
To bundle your assets into your final binary in release mode, you must:
REMOVE any calls to the
vial::asset_dir!()
macro.Add
vial
as to[build-dependencies]
in yourCargo.toml
:
[build-dependencies]
vial = "0.1"
(Yes, you should now have vial
in there twice. Once
for build-dependencies
and once for
dependencies
. Only the one under dependencies
needs to list any optional features you want to use, however.)
- Create a
build.rs
in the root of your project and callvial::bundle_assets!()
in it, passing your asset directory as the sole argument:
fn main() {
vial::bundle_assets!("assets/").unwrap();
}
⚠️ Note: Bundling assets and setting an asset path
using vial::asset_dir!()
are mutually exclusive - you can’t
do both, as enabling bundling will set the asset path for you. Therefor
if you are making the transition from using-assets-but-not-bundling to
using-assets-and-bundling-them, make sure to remove your call to
vial::asset_dir!
.
Other than that, you’re all set! Your application will now bundle
your assets in --release
mode and use the disk in debug and
test mode.
All calls to functions in the assets module should work with the files in your asset directory. Add more and get to it!
State
There are two types of state available in Vial:
Local State - Built-in to Request. Allows caching of expensive algorithms (like DB lookups) on a per-request basis.
Global State - Allows you to share any
Send + Sync + 'static
types (like database connections) across all requests.
Local State
Local state lives for only a single Request,
but can be useful to prevent looking up the same data over and over. The
cache is based on the return type of the function or closure you pass to
cache()
, so make sure to create little wrapper structs if
you want different functions to return the same type, like
Vec<String>
:
struct PageNames(Vec<String>);
struct UserNames(Vec<String>);
Here’s an example:
use vial::prelude::*;
use page::Page;
use db;
routes! {
"/" => list;
GET }
struct PageNames(Vec<String>);
fn all_pages(_: &Request) -> Vec<Page> {
db::lookup("select * from pages")
}
fn page_names(req: &Request) -> PageNames {
.cache(all_pages)
PageNames(req.iter()
.map(|page| page.name.clone())
.collect::<Vec<_>>())
}
fn list_of_names(req: &Request) -> String {
.cache(page_names)
req.0
.iter()
.map(|name| format!("<li>{}</li>", name))
.collect::<Vec<_>>()
.join("\n")
}
fn list(req: Request) -> impl Responder {
format!(
"<html>
<head><title>{title}</title></head>
<body>
<h1>{title}</h1>
<h3>There are {page_count} pages:</h3>
<ul>
{pages}
</ul>
</body>
</html>",
= "List Pages",
title = req.cache(all_pages).len(),
page_count = req.cache(list_of_names),
pages
)}
fn main() {
run!().unwrap();
}
Global State
There are two steps involved in setting up shared, global state in Vial:
- Create a struct that is
Send + Sync
to hold your application’s shared state:
use vial;
use std::sync::{Arc, Mutx, atomic::{AtomicUsize, Ordering}};
use some_db_crate::DB;
struct MyConfig {
: Arc<Mutex<DB>>,
db: AtomicUsize,
counter}
impl MyConfig {
pub fn new(db: DB) {
{
MyConfig : Arc::new(Mutex::new(db)),
db: AtomicUsize::new(0),
counter}
}
}
- Tell Vial about your state object before calling
run!
:
fn main() {
let db = DB::new();
vial::use_state!(MyConfig::new(db));
vial::run!();
}
Now your actions and filters can access MyConfig
by
calling the state()
method on Request:
use vial::prelude::*;
routes! {
"/list" => list;
GET }
fn find_names(db: Arc<Mutex<DB>>) -> Result<Vec<String>, db::Error> {
Ok(db.lock()?.query("SELECT name FROM names")?
.map(|row| row.get("name")?)
.collect::<Vec<_>>())
}
fn list(req: Request) -> Result<String> {
Ok(
.state::<MyConfig>().db.clone())?
find_names(req.map(|name| format!("<li>{}</li>", name))
.join("\n")
)}
You might find it more convenient to define and implement a local
trait on the Request struct instead of calling
request.state()
directly:
use vial::prelude::*;
routes! {
"/list" => list;
GET }
// ...
trait WithConfig {
fn config(&self) -> &MyConfig;
}
impl WithConfig for vial::Request {
fn config(&self) -> &MyConfig {
self.state::<MyConfig>()
}
}
fn list(req: Request) -> Result<String> {
Ok(
.config().db.clone())?
find_names(req.map(|name| format!("<li>{}</li>", name))
.join("\n")
)}
Templates
Optional Feature: Coming soon.
Cookies
Extremely basic cookie support is available by enabling the
cookies
feature in your Cargo.toml
:
[Dependencies]
vial = { version = "*", features = ['cookies'] }
Once it’s enabled you can access cookies the client sent using
req.cookie(name)
, set cookies using a similar API to the Request Headers API, or remove cookies using
remove_cookie()
or without_cookie()
:
use vial::prelude::*;
routes! {
"/" => show;
GET "/clear" => clear;
GET "/set/:count" => set;
GET }
fn show(req: Request) -> impl Responder {
let count: usize = req.cookie("count").unwrap_or("0").parse().unwrap();
let new_count = count + 1;
Response::from(format!("count: {}", count))
.with_cookie("count", new_count.to_string())
}
fn clear(req: Request) -> impl Responder {
Response::redirect_to("/").without_cookie("count")
}
fn set(req: Request) -> Option<impl Responder> {
let val: usize = req.arg("count")?.parse().unwrap();
Response::redirect_to("/")
.with_cookie("count", val.to_string()).into()
}
fn main() {
run!();
}
Like all HTTP key/value pairs, cookie names are case insensitive.
Sessions
Sessions are essentially encrypted cookies that live until the user closes their browser. As a result, they can be treated as semi- trustworthy and store information like a username or preference.
To enable sessions, enable the sessions
feature in your
Cargo.toml
:
[Dependencies]
vial = { version = "*", features = ['sessions'] }
Note that this also turns on the cookies
feature, as
sessions depend on cookies.
Once it’s enabled you can access the client’s session using an API
similar to cookie’s: req.session(name)
. You can set session
values using a similar API to the Request
Headers API. Removing session values is done using
remove_session()
or without_session()
:
use vial::prelude::*;
routes! {
"/" => show;
GET "/clear" => clear;
GET "/set" => set;
GET }
fn show(req: Request) -> impl Responder {
let count: usize = req.session("count").unwrap_or("0").parse().unwrap();
let new_count = count + 1;
Response::from_session("count", &new_count.to_string()).with_body(format!(
r#"
<h1> Count: {} </h1>
<p><a href="/clear">Clear Count</a></p>
<form action="/set" method="GET">
<input type="text" name="count" />
<input type="submit" value="Set Count" />
</form>
"#,
new_count
))}
fn clear(_req: Request) -> impl Responder {
Response::redirect_to("/").without_session("count")
}
fn set(req: Request) -> impl Responder {
let mut res = Response::redirect_to("/");
if let Some(val) = req.query("count").map(|c| c.parse::<usize>().unwrap_or(0)) {
let val = val.to_string();
.set_session("count", &val);
res}
res}
fn main() {
run!();
}
Like all HTTP key/value pairs, session names are case insensitive.
JSON
Vial supports JSON requests and responses via Serde and nanoserde.
First, enable either json_serde
or
json_nano
in your Cargo.toml
:
[Dependencies]
vial = { version = "*", features = ['json_serde'] }
Now, you can use Request::json
to deserialize a JSON
request body, and Response::with_json
to serialize a JSON
response body:
// serde example
use vial::prelude::*;
routes! {
"/json" => post;
POST }
fn post(req: Request) -> impl Responder {
match req
.json::<serde_json::Value>()
.ok()
.as_ref()
.and_then(|val| val.as_object())
.and_then(|obj| obj.get("message"))
.and_then(|val| val.as_str())
.map(|message| message.to_string())
{
Some(message) => Response::from(200).with_json(serde_json::json!({
"message": format!("Echo: {}", message)
})),
None => Response::from(400).with_body("json request parse error"),
}
}
fn main() {
vial::run!().unwrap();
}
Serde’s derive macro can
conveniently generate code to serialize and deserialize structs and
enums, including helpful error messages. To use it, add a dependency on
serde
with the derive
feature enabled in your
Cargo.toml
:
[Dependencies]
serde = { version = "*", features=["derive"] }
Now, you can use Request::json
and
Response::with_json
with any type implementing
serde::Deserialize
and serde::Serialize
,
respectively:
use vial::prelude::*;
routes! {
"/json" => post;
POST }
#[derive(serde::Serialize, serde::Deserialize)]
struct Echo {
: String,
message}
fn post(req: Request) -> impl Responder {
match req.json::<Echo>() {
Ok(echo) => Response::from(200).with_json(Echo {
: format!("Echo: {}", echo.message),
message}),
Err(e) => Response::from(400).with_body(e.to_string()),
}
}
fn main() {
vial::run!().unwrap();
}
nanoserde has a similar derive macro available:
#[derive(SerJson, DeJson)]
struct Message {
: String,
message}
Database
“Pro Tip”: Coming soon.
Markdown
“Pro Tip”: Coming soon.