Web Dev

Gotham – From Start to Heroku

In this article, we’ll implement and deploy a Gotham full-stack web framework using the Tera template system, Webpack for a complete front-end asset management, a minimal VueJS and CoffeeScript web app and deploy to Heroku. Gotham is a Rust framework which is focused on safety, speed, concurrency and async everything. Webpack is a NodeJS website asset preprocessor and bundler which can let you use any of your favorite front end technologies. Combining these technologies allow for a small footprint on the server, which means saving money on infrastructure, very fast performance in page load for higher visitor retention and the full flexibility of client side code available to you for web design without limitations.

Because there is a lot to unpack here, this article will cover a step-by-step guide to launch these features on Heroku and advise you on common issues that need to be considered.

Installing the dependencies

Before we worry about the server environment, we need to be able to run the server on our own system. You will need to install each of the following:

Since the kind of installation you have to go through depends on your operating system, simply follow the steps provided in the links above for each item.

Setting up the project

First we generate a Rust project with

cargo new mouse
cd mouse

We’ll call this project mouse, as in Mighty Mouse. Next we’ll use WebpackerCli to install the initial files for using Webpacker in our project.

webpacker-cli init

Next we’ll edit our Cargo.toml to add the dependencies we need. Add the following to the end of it.

[dependencies]
gotham = "0.3.0"
gotham_derive = "0.3.0"

hyper = "0.12.13"
mime = "0.3.12"
lazy_static = "1.2"
tera = "0.11"
webpacker = "~0.3"

[build-dependencies]
webpacker = "~0.3"

Now add the following to your build.rs file in the projects main directory.

extern crate webpacker;

fn main() {
    println!("Validating dependencies…");
    assert!(webpacker::valid_project_dir());
    
    println!("Compiling assets…");
    let _ = webpacker::compile();
}

Now whenever you run the cargo command to build your project, it will verify your dependencies, bundle and prepare your assets. This is very helpful when you deploy to Heroku as it will tell you which dependencies are missing.

A working project

The hello world example given on the main page for Gotham is as follows. Put this in your src/main.rs file.

extern crate gotham;

use gotham::state::State;

const HELLO_WORLD: &'static str = "Hello World!";

pub fn say_hello(state: State) -> (State, &'static str) {
    (state, HELLO_WORLD)
}

pub fn main() {
    let addr = "127.0.0.1:7878";
    println!("Listening for requests at http://{}", addr);
    gotham::start(addr, || Ok(say_hello))
}

At this point you can run cargo run and use your browser to navigate to http://127.0.0.1:7878 to see the hello world example.

From here we’re going to remove the const HELLO_WORLD line and the entire say_hello method. We’ll add a method named index_page, add a method named router, and we’ll update the last line of main method to use them.

extern crate gotham;
extern crate hyper;

use gotham::state::State;
use gotham::router::builder::{
    build_simple_router,
    DefineSingleRoute,
    DrawRoutes
};
use gotham::router::Router;
use hyper::Method;

pub fn index_page(state: State) -> (State, (mime::Mime, String)) {
    let rendered = "Hello World!".to_string();

    (state, (mime::TEXT_HTML, rendered))
}

pub fn router() -> Router {
    build_simple_router(|route| {
        route.
            request(vec![Method::GET, Method::HEAD], "/").
            to(index_page);
    })  
}

pub fn main() {
    let addr = "127.0.0.1:7878";
    println!("Listening for requests at http://{}", addr);
    gotham::start(addr, router())
}

This changes introduce mime type support in the method which we now use a router to get to. The router is mapping any request to the root url / to the index_page method.

For this project we’ll follow Rails’ outline for organizing the files for the site.

Now to demonstrate how to serve static assets in Gotham. Create the following directory structure app/assets/stylesheets in the root of your project. Create a file in that last directory named application.css and give it some styles like so.

div {
  margin: 0 12px 0 12px;
}

footer {
  margin-top: 40px;
  font-size: 6pt;
}

Towards the top of src/main.rs add use
gotham::handler::assets::FileOptions;
 and inside the build_simple_router code block add the following route option after the one you currently have in there.

route.
    get("style/*").
    to_dir(
        FileOptions::new("app/assets/stylesheets").
            with_cache_control("no-cache").
            with_gzip(true).
            build(),
    );

This will route any requests that try to access the style/ path in the url to any file that’s in app/assets/stylesheets. In our HTML code we’ll link to this style directly. Before that though you can now try to load the url http://127.0.0.1:7878/style/application.css after you run cargo run and see the styles we’ve entered in.

We’re now ready to introduce HTML pages with the Tera templating system.

Tera templating in Gotham

Tera is a templating DSL for Rust which serializes Rust objects before processing the views. There’s very little learning curve to using it as it is designed with common template tasks in mind.

We’ll rewrite the index_page method to now use Tera and include the stylesheet we’ve created. Also we’ll create a core object to load all our templates from and provide it with a path for our views. In your src/main.rs file update it for the following.

#[macro_use]
extern crate lazy_static;
extern crate tera;
use tera::{Context, Tera};

lazy_static! {
    pub static ref TERA: Tera =
        Tera::new("app/views/**/*.tera").
            map_err(|e| {
                eprintln!("Parsing error(s): {}", e);
                ::std::process::exit(1);
            }).
            unwrap();
}

pub fn index_page(state: State) -> (State, (mime::Mime, String)) {
    let mut context = Context::new();

    let styles = &["style/application.css"];
    let sources: &[&'static str] = &[];
    context.insert("application_styles", styles);
    context.insert("application_sources", sources);

    let rendered = TERA.render("landing_page/index.html.tera", &context).unwrap();
    (state, (mime::TEXT_HTML, rendered))
}

In the lazy_static! block we create the TERA object which will load all the views and templates into an internal hash like lookup system and by which we will use it to render views with given contexts. The context we provide a view will contain the objects the view are to be updated or generated with.

Now we need to create our application template and our landing page for the above code to work. Create the file app/views/layouts/application.html.tera

<!DOCTYPE html>
<html lang="en">
<head>
  {% block head %}{% for style in application_styles -%}
    <link rel="stylesheet" href="{{ style }}" />
  {% endfor %}{% for source in application_sources -%}
    <script src="{{ source }}"></script>
  {% endfor %}
  <title>{% block title %}{% endblock title %}</title>
  {% endblock head %}
</head>
<body>
  <header>
    {% block header %}{% endblock header %}
  </header>
  <div id="content">
    {% block content %}{% endblock content %}
  </div>
  <footer>
    {% block footer %}© Copyright 2018{% endblock footer %}
  </footer>
</body>
</html>

The block and endblock tags allow you to place default page content into each section while also allowing any pages that inherit this template to optionally overwrite it, or even add to it by calling super first. The for loops above use a value provided from context insertion from our code — and those must exist. The forloop then iterates over the collection of items provided and can be used to insert text internally with the {{ }} syntax.

Now let’s create the index page that inherits from this. Create the file app/views/landing_page/index.html.tera and place the following in it.

{% extends "layouts/application.html.tera" %}

{% block title %}Mouse{% endblock title %}

{% block content %}

  <h1>Mouse</h1>
  You have arrived.

{% endblock content %}

Here we use Tera’s extend keyword to choose the template we wish to employ and then we replace to blocks from the template, title and content. Go ahead and spin up the server and try it out; cargo run and navigate your browser to http://127.0.0.1:7878. You have now successfully deployed templating.

Adding VueJS and CoffeeScript support

Most of the tooling for this was done when we added the webpacker crate, installed WebpackerCli, and added build hooks in the build.rs code. We have three parts now to do

  • Routing for the Webpack’d assets
  • Implement helper methods for looking up assets from the manifest
  • Install and configure the Webpack dependencies for VueJS & CoffeeScript

Routing is simple as we will be following the same technique we used for our stylesheet earlier. We’ll set our webpack’d assets routing url path to a globally available static value for convenience should we ever desire to change it. In our src/main.rs file add:

pub static ASSET_DIRECTORY: &'static str = "public";

And add the following to the build_simple_router routing block.

route.
    get(&format!("{}/*", &ASSET_DIRECTORY)).
    to_dir(
        FileOptions::new("public").
            with_cache_control("no-cache").
            with_gzip(true).
            build(),
    );

Note the path we use to share publicly doesn’t have to be the same name as the directory in which webpacker prepares the assets.

Now with the first step done lets work on our manifest and helper methods. In the same lazy_static block we defined TERA earlier add the following:

extern crate webpacker;
use webpacker::Manifest;

lazy_static! {
    pub static ref MANIFEST: Manifest =
        webpacker::manifest(None).unwrap();
}

This creates a hash lookup table for each of our assets that Webpack has preprocessed and prepared. Now let’s add our helper methods to simplify getting a full routing path from a given file name. In src/main.rs add the following.

use webpacker::asset_path::AssetPath;
use std::ops::Deref;

pub fn asset_source(key: &str) -> String {
    AssetPath::new(ASSET_DIRECTORY, key, MANIFEST.deref()).into()
}

pub fn javascript_pack_tag(key: &str) -> String {
    let key = format!("{}.js", basename(key));
    asset_source(&key)
}

pub fn stylesheet_pack_tag(key: &str) -> String {
    let key = format!("{}.css", basename(key));
    asset_source(&key)
}

fn basename<'a>(s: &'a str) -> &'a str {
    s.rsplit('/').next().unwrap().
        split('.').next().unwrap()
}

When Webpack processes CoffeeScript files the result will be a JavaScript file so we can give our helper method the original filename example.coffee and it changes it to example.js as the key to lookup from the file manifest which then gives us a full path such as /packs/example-779d5e71ab17b09de712.js and that source path gets inserted into our webpage providing both pre-processing and cache invalidation. The same thing also occurs with stylesheets from SASS files to CSS. Using these helper methods to provide the full link path to our web document source references provides an up to date code experience for the end user.

Now that we have our asset file manifest helper methods we can include the default JavaScript script that Webpacker added by changing the sources line in our index_page method to the following:

let sources = &[&asset_source("application.js")];

Now when you run your program with cargo run you can look at the source code of the page and it now includes a valid script inclusion for a versioned application.jsfile.

Now that the second step is done let’s do the third step of installing and configuring CoffeeScript and VueJS for Webpacker. Ideally you can just run webpacker-cli install:vue and webpacker-cli install:coffee. This will create the configuration files for you but be sure to pay attention as to whether the Yarn dependencies are installed. As of this writing VueJS dependencies are requiring you to manually select the version number for vue-loader. The output displays the command you should run to add the Yarn dependencies.

If you want to manually install VueJS then you could do it as follows. Run

yarn add vue vue-loader@15.4.2 vue-template-compiler

Create the file config/webpack/loaders/vue.js and write in the following.

module.exports = {
  test: /\.vue$/,
  use: [{
    loader: 'vue-loader'
  }]
}

And update the config/webpack/environments.js file to add:

const { VueLoaderPlugin } = require('vue-loader')
const vue =  require('./loaders/vue')

environment.plugins.append('VueLoaderPlugin', new VueLoaderPlugin())
environment.loaders.append('vue', vue)

Add the above after webpacker is included and before the module exports line. And that’s how easy it is to manually add support for something to Webpacker. Now whenever you write VueJS code in your app/javascript/packs folder you can include it with the _pack helper methods we wrote earlier.

VueJS and CoffeeScript example

The install command for WebpackerCli creates some examples under both app/javascript and app/javascript/packs. We will use the app/javascript/app.vueexample as it is and if you didn’t generate one with the install command here it is below:

<template>
  <div id="app">
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      message: "Hello Vue!"
    }   
  }
}
</script>

<style scoped>
p {
  font-size: 2em;
  text-align: center;
}
</style>

Now delete both app/javascript/hello_vue.js and app/javascript/hello_coffee.coffee and create the file app/javascript/hello.coffee and place the following in it.

import Vue from 'vue/dist/vue.esm'
import App from '../app.vue'

document.addEventListener('DOMContentLoaded', ->
  element = document.getElementById 'vue-app'

  if element?
    app = new Vue(
      el: element
      render: (h) -> h App 
    ) 

  # Vue.config.devtools = true
)

In my experimentation the environment JavaScript detects it keeps reporting production regardless of changing local environment variables so if you’d like to use the VueJS Devtool addon for your browser you should uncomment the devtools line above while you work.

Now that we have the code to test we need only to include it in our site. Let’s change our styles and sources values in the index_page method in src/main.rs to the following:

let sources = &[
    &asset_source("application.js"),
    &javascript_pack_tag("hello")
];  

let styles = &[
    "style/application.css",
    &stylesheet_pack_tag("hello")
]; 

We’re including both the script and style for our VueJS code by using our _packmethod helpers for the hello.coffee file. Now add the following to app/views/landing_page/index.html.tera within the content block.

<div id="vue-app"></div>

And now you have a working VueJS & CoffeeScript app when you run cargo
run
 and view http://127.0.0.1:7878 .

Deploying to Heroku

To deploy to Heroku you will need to use three separate buildpacks together for NodeJS, Ruby, and Rust. First let’s initialize Heroku in our project. Use the Heroku Cli tool:

heroku create
heroku buildpacks:add heroku/nodejs
heroku buildpacks:add heroku/ruby
heroku buildpacks:add emk/rust

It’s very important to include Rust as the last buildpack.

Before we can deploy we need to change the way our program hosts it’s IP and PORT. Open the src/main.rs file and change the main method to the following.

use std::env;

pub fn main() {
    let port = env::var("PORT").expect("PORT env not found!");
    let addr = format!("0.0.0.0:{}", port);
    println!("Listening for requests at {}", addr);
    gotham::start(addr, router())
}

The application won’t work on Heroku without binding to both the address 0.0.0.0and the port number defined by the environment variable. Now to test it locally you have to assign a port number so the command in bash would look like PORT=7878 cargo run.

Next we have to let Heroku know what command to run to run the application. Open up a file name Procfile and place the following.

<

pre>web: target/release/mouse

The last part is of course the name of the application which we gave it; mouse. Now you need only to commit the source code with git and upload it to heroku.

Be sure your .gitignore file has lines for node_modules and tmp as you don’t want to upload those.

git add .
git commit -m "Heroku ready"
git push heroku master

After time enough to brew coffee you can now open the deployed website with the command heroku open. And viola! You’ve achieved implementing and deploying a fullstack Gotham app.

As your application grows it will help to organize similar source code categories together in their own separate files (such as moving routing to src/route.rs).

Summary

This how-to should save you tons of time figuring out how to get a fullstack Gotham app ready. You can view the source code for this example here on Github.

What you have here is a quicker way to get up and going with a very fast and capable website. Fast being what Rust and Gotham bring to the table and capable being what Webpacker and the entire JavaScript ecosystem bring with it. When you use Rust for your website you get the best performance you can in delivery. Any slowness experienced will be from other factors like unoptimized database queries or network latency. It’s exciting to be working with both powerful and performant technologies when delivering content. Enjoy!

Published on Web Code Geeks with permission by Daniel P. Clark, partner at our WCG program. See the original article here: Gotham — From Start to Heroku

Opinions expressed by Web Code Geeks contributors are their own.

Daniel P. Clark

Daniel P. Clark is a freelance developer, as well as a Ruby and Rust enthusiast. He writes about Ruby on his personal site.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button