A Simple Web App in Rust, Part 1
Table of Contents
1 Intro & Background
What is it like to write a tiny web app in Rust from the perspective of an experienced programmer who is new to the ecosystem? Read on to find out.
I've been interested in Rust since I first heard about it. A systems language that supports macros & has room to grow towards higher-level abstractions? Awesome.
So far, I have only written read blog posts about Rust & done some very basic "hello world" style programs. So, I guess I'm saying that my perspective is pretty raw.
A while back I saw this article about learning Racket, and I thought it was really great. We need more people writing about their experiences as beginners with a technology, especially those who already have a fair amount of experience with technology 1. I also liked its stream-of-consciousness approach, and think it would be a nice experiment to write one for Rust.
So, with the preliminaries out of the way, let's get started.
2 The App
The app I want to build serves a simple need of mine: A brain-dead-easy way to record when I take my medication each day. I want tap a link on my home screen and have it record the visit, and this will preserve a record of when I've taken my medication.
Rust seems to be suited for this app. It's fast. Running a single, simple server takes relatively few resources, so it won't be taxing to my VPS. And, I have wanted to do something more real with Rust.
The MVP is very small, but there room for it to grow if I want to add more features. Sounds perfect.
3 The Plan
So, I'm going to quickly admit something here: I lost an earlier version of this project. This has some disadvantages: as I recreate this, I won't have the same level of unfamiliarity I did when I approached it some weeks ago. However, I think I remember those pain points, and will do my best to recreate them.
However, there is one thing that I learned that I want to apply here: it is much easier to build separate, individual programs while exploring APIs instead of trying to do everything all at once.
To that end, I have the following plan:
- Build a simple web server that displays "hello world" when I visit.
- Build a tiny program that logs the formatted date and time whenever it is run.
- Integrate the two into a single application.
- Deploy this application to my server, a Digital Ocean VPS.
4 Writing The "Hello World" Web App
So, I'm starting an empty git repo & have homebrew installed. Lets install Rust. I know this much, at least.
4.1 Installing Rust
$ brew update ... $ brew install rust ==> Downloading https://homebrew.bintray.com/bottles/rust-1.0.0.yosemite.bottle.tar.gz ############################################################################################# 100.0% ==> Pouring rust-1.0.0.yosemite.bottle.tar.gz ==> Caveats Bash completion has been installed to: /usr/local/etc/bash_completion.d zsh completion has been installed to: /usr/local/share/zsh/site-functions ==> Summary /usr/local/Cellar/rust/1.0.0: 13947 files, 353M
Oook, before anything else, lets do a regular "hello world" program.
$ cat > hello_world.rs fn main() { println!("hello world"); } ^D $ rustc hello_world.rs $ ./hello_world hello world $
So far, so good. Rust is working! Or, at least, the compiler is.
A friend suggested I try nickle.rs as a web application framework for Rust. It looks good to me!
As of today, the first example it uses is:
#[macro_use] extern crate nickel; use nickel::Nickel; fn main() { let mut server = Nickel::new(); server.utilize(router! { get "**" => |_req, _res| { "Hello world!" } }); server.listen("127.0.0.1:6767"); }
So, the first time I did this, I got a little side tracked and learned a bit about cargo. This time, I notice that there's this "getting started" link, so I think I'll try that instead of getting everything set up on my own.
There's a script that I'm supposed to curl
and pipe into a root
shell, but that makes me paranoid so I'm going to download it and look
over it first.
curl -LO https://static.rust-lang.org/rustup.sh
Ok, this actually doesn't look like its going to do what I want. At
least, there's a lot going on in this script, more than I want to deal
with right now. Hmm. I wonder if cargo
got installed with rustc
?
$ which cargo /usr/local/bin/cargo $ cargo -v Rust's package manager Usage: cargo <command> [<args>...] cargo [options] Options: -h, --help Display this message -V, --version Print version info and exit --list List installed commands -v, --verbose Use verbose output Some common cargo commands are: build Compile the current project clean Remove the target directory doc Build this project's and its dependencies' documentation new Create a new cargo project run Build and execute src/main.rs test Run the tests bench Run the benchmarks update Update dependencies listed in Cargo.lock search Search registry for crates See 'cargo help <command>' for more information on a specific command.
Ok, that looks good I guess? I'll go with it for now.
$ rm rustup.sh
4.2 Setting Up the Project
So, the next step is to generate a new project directory. But I already have a project directory =(. I'll try it like this, anyway.
$ cargo new . --bin Destination `/Users/joel/Projects/simplelog/.` already exists
Ugh, that didn't work.
$ cargo -h Create a new cargo package at <path> Usage: cargo new [options] <path> cargo new -h | --help Options: -h, --help Print this message --vcs <vcs> Initialize a new repository for the given version control system (git or hg) or do not initialize any version control at all (none) overriding a global configuration. --bin Use a binary instead of a library template --name <name> Set the resulting package name -v, --verbose Use verbose output
Yeah, it doesn't look like this is going to do what I want it to. Blah I guess I'll just redo this repo.
$ cd ../ $ rm -rf simplelog/ $ cargo new simple-log --bin $ cd simple-log/
Ok, what's up in here?
$ tree . |____.git | |____config | |____description | |____HEAD | |____hooks | | |____README.sample | |____info | | |____exclude | |____objects | | |____info | | |____pack | |____refs | | |____heads | | |____tags |____.gitignore |____Cargo.toml |____src | |____main.rs
So, it made a git repository, Cargo.toml
and a main.rs
in a src
directory. Sounds fine to me.
Next on the nickel getting started page,
I add the nickel.rs
dependency to the cargo file, and it now looks
like:
[package] name = "simple-log" version = "0.1.0" authors = ["Joel McCracken <mccracken.joel@gmail.com>"] [dependencies.nickel] git = "https://github.com/nickel-org/nickel.rs.git"
This makes sense, I think. I'm not sure what
"dependencies.nickel" is actually saying. Is dependencies
a hash
with a nickel
key? I do get that I'm saying "get nickel from here,
yo".
4.3 The "Hello World" Example Running
Anyway, moving on and pasting the example into main.rs
:
#[macro_use] extern crate nickel; use nickel::Nickel; fn main() { let mut server = Nickel::new(); server.utilize(router! { get "**" => |_req, _res| { "Hello world!" } }); server.listen("127.0.0.1:6767"); }
So, what's the deal with macro_use
, extern
, and then needing to
use
? All stuff I will figure out later.
Oh snap, I had a few sentences about how macro_use
was probably a
macro-time directive2, but I didn't see any actual
macros, so I deleted it. Now I see router!
, though, so I'm even more
inclined to think this is a macro directive. Also, I doubt
the term 'directive' is correct, so don't pick it up.
Lets try it:
cargo run Updating git repository `https://github.com/nickel-org/nickel.rs.git` Updating registry `https://github.com/rust-lang/crates.io-index` Downloading unsafe-any v0.4.1 Downloading httparse v0.1.2 Downloading traitobject v0.0.3 Downloading lazy_static v0.1.10 Downloading pkg-config v0.3.4 Downloading num_cpus v0.2.5 Downloading modifier v0.1.0 Downloading groupable v0.2.0 Downloading unicase v0.1.0 Downloading gcc v0.3.5 Downloading time v0.1.25 Downloading log v0.3.1 Downloading url v0.2.34 Downloading plugin v0.2.6 Downloading hyper v0.4.0 Downloading matches v0.1.2 Downloading mustache v0.6.1 Downloading bitflags v0.1.1 Downloading typeable v0.1.1 Downloading openssl v0.6.2 Downloading rustc-serialize v0.3.14 Downloading typemap v0.3.2 Downloading regex v0.1.30 Downloading cookie v0.1.20 Downloading mime v0.0.11 Downloading libc v0.1.8 Downloading openssl-sys v0.6.2 Compiling modifier v0.1.0 Compiling traitobject v0.0.3 Compiling regex v0.1.30 Compiling libc v0.1.8 Compiling lazy_static v0.1.10 Compiling matches v0.1.2 Compiling httparse v0.1.2 Compiling rustc-serialize v0.3.14 Compiling groupable v0.2.0 Compiling pkg-config v0.3.4 Compiling gcc v0.3.5 Compiling bitflags v0.1.1 Compiling unicase v0.1.0 Compiling typeable v0.1.1 Compiling unsafe-any v0.4.1 Compiling log v0.3.1 Compiling num_cpus v0.2.5 Compiling typemap v0.3.2 Compiling mime v0.0.11 Compiling plugin v0.2.6 Compiling openssl-sys v0.6.2 Compiling time v0.1.25 Compiling openssl v0.6.2 Compiling url v0.2.34 Compiling mustache v0.6.1 Compiling cookie v0.1.20 Compiling hyper v0.4.0 Compiling nickel v0.5.0 (https://github.com/nickel-org/nickel.rs.git#69546f58) Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) Running `target/debug/simple-log` Listening on http://127.0.0.1:6767 Ctrl-C to shutdown server ^C
Woo hoo! Visiting localhost:6767
in my browser worked.
4.4 One Final Challenge
Ok, now I want to try one thing and then call it a night: can I move the "hello world" into its own function? Baby steps, after all.
fn say_hello() { "Hello dear world!"; } fn main() { let mut server = Nickel::new(); server.utilize(router! { get "**" => |_req, _res| { say_hello(); } }); server.listen("127.0.0.1:6767"); }
Err… when I run it this time, I see "Not found". Let me try fiddling with semicolons just in case that's important:
fn say_hello() { "Hello dear world!" } fn main() { let mut server = Nickel::new(); server.utilize(router! { get "**" => |_req, _res| { say_hello() } }); server.listen("127.0.0.1:6767"); }
Ok… now I have a completely different error message:
$ cargo run Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:6:5: 6:24 error: mismatched types: expected `()`, found `&'static str` (expected (), found &-ptr) [E0308] src/main.rs:6 "Hello dear world!" ^~~~~~~~~~~~~~~~~~~ error: aborting due to previous error Could not compile `simple-log`. To learn more, run the command again with --verbose.
So, I guess the presence/absence of the semicolon was important. And
now I'm getting a type error, at least. Oh, and I'm 90% sure that ()
is referring to what I remember to be 'unit', the Rust idea of
undefined, nil, or whatever. I'm sure this isn't quite right, but I
guess it makes sense.
I assumed Rust would do type inferencing. Does it not? Or does it just not do it around function boundaries? Hmm.
So, the error message is telling me that it expected the return value to be unit, but the actual return value was a static string(?). I'm pretty sure I've seen the syntax for specifying return value types; let me see:
#[macro_use] extern crate nickel; use nickel::Nickel; fn say_hello() -> &'static str { "Hello dear world!" } fn main() { let mut server = Nickel::new(); server.utilize(router! { get "**" => |_req, _res| { say_hello() } }); server.listen("127.0.0.1:6767"); }
The type &'static str
looks very weird to me. Does it compile? Does
it work?
$ cargo run & [1] 14997 Running `target/debug/simple-log` Listening on http://127.0.0.1:6767 Ctrl-C to shutdown server $ curl http://localhost:6767 Hello dear world! $ fg cargo run ^C
Yay, it worked! This time around, Rust hasn't been that frustrating. I'm not sure if its because I'm more familiar with some of this tooling, or I've opted to read documentation more, but I'm having fun. Also, the difference between reading a language and writing in a language sometimes very surprising. While I understand these code examples, I can't make edits quickly and effectively.
—
Next up, we will work through the process of writing the current date to a file. This can be found here.
—
Series: A Simple Web App in Rust
Footnotes:
I'm not trying to say that the experiences of beginners is not valuable – far from it! However, I do think those experiences bring a separate set of insights than those from someone who has been programming for a long time, and they may notice how non-standard some things in an ecosystem are.
I would normally say 'compile-time' directive, but that doesn't make much sense since Rust is a compiled language. So, I say 'macro-time' directive, but I really have no idea.