DEV Community

Doordashcon
Doordashcon

Posted on • Updated on

Scaffolding

What will be the prominent development stack starting 2022? Most recently blockchain development has been gaining traction amidst the bearish market this year, why? To put it simply there are a lot of new developers who would rather learn something new than work with legacy systems, I'm talking about me specifically 👋. Been coding as a hobby since January 2019, started with data science didn't like it then moved to web development in Python and got stuck because I was having trouble grasping the class system, all I built back then were toys. Fast forward 2020 last quarter when I found out about blockchain tech, it looked so shinny to me so I just jumped right in and since then I've joined hackathons(first time) built projects and connected with people on a mutual level thanks to blockchain tech, I mainly used Solidity + React, these took me little time to understand compared to Python in data science and Python in web development but now I'm looking for flexibility(to use a single language in multiple areas, not just blockchain programming) and interoperability(to use the same language across multiple blockchains).
Welcome Rust! A language where data fields and methods are not in a single construct(i.e class) but independent of each other(i.e struct for data and trait for functions/methods) until implementation is required, essentially completing the same task with better ergonomics but these aren't the only good things about Rust which I will demonstrate by creating a web server in Rust and a frontend in React adding up to make an RSVP website(skeleton)

Going to need

Rust & Nodejs installed on your computer, these come with their package managers.

Flow of the service

user requests invite

invitation is sent to the user(i will only be providing how to get unique id's for each request)

users register with their unique id and after a certain amount of registrations no invite will be sent out neither will any unregistered user be allowed to register

registered users will be allowed to attend

Checking the versions installed & creating a new project

# at the time of writing this my setup is
rustc --version && cargo --version && node -v && npm -v
# rustc 1.53.0 (53cb7b09b 2021-06-17)
# cargo 1.53.0 (4369396ce 2021-04-27)
# v14.15.5
# 6.14.11

# Create a new project
cargo new rsvp
# Created binary (application) `rsvp` package

cd simple-auth-server # and then

# `cargo install cargo-watch` if you haven't already
# watch for changes re-compile and run
cargo watch -x run
Enter fullscreen mode Exit fullscreen mode

a rust binary produce an executable file and has a file main.rs in the src directory where all other .rs files run.

Modify the main function

// src/main.rs
fn main() -> std::io::Result<()> {

}
Enter fullscreen mode Exit fullscreen mode

in rust we model success or failure with the return types, one of which is the Result type encapsulating two values Result<T, E>. These values communicate either an error err(E) or a success Ok(T) but the main function is special and should return an empty tuple, the syntax std::io before it simple tells us we are using rust's standard input/output Result type and not something that magically appeared.

Next we have the cargo.toml file in the top level directory

[package]
name = "rsvp"
version = "0.1.0"
edition = "2018"

[dependencies]
Enter fullscreen mode Exit fullscreen mode

Called a manifest and generated by running the command cargo new, containing important info(separated into section) about our project. the [package] section contains default metadata, more can be added and then there's the [Dependencies] section which will contain essential components needed to build out the server.

Defining our file system

// src/main.rs
mod attendance_handler;
mod errors;
mod invitation_handler;
mod models;
mod register_handler;
mod schema;
mod utils;

...
Enter fullscreen mode Exit fullscreen mode

What the keyword mod does is create a module from whatever rust file it's pointing to, making it's items: functions, structs, traits, impl blocks, and even other modules accessible in this file provided they contain the keyword pub which grants public access. All the mod declarations are pointing to rust files in the src directory.

Database

A web application wont be complete without one, using a nifty tool called diesel we'll be able to create database tables, schemas and make queries in Rust but first we need a database installed, ill be using postgresql, also make sure you install just the postgresql feature of diesel.

Create a database & database tables

# make sure the database is running
# replace 'username' with the default account name given after installation 'postgres', 'password' with the system password and 'database_name' with whatever name preferred for a database.
echo DATABASE_URL=postgres://username:password@localhost/database_name > .env

# This will create our database if didn’t exist and setup a migration directory
diesel setup

# Create database tables
diesel migration generate users
diesel migration generate invitations
Enter fullscreen mode Exit fullscreen mode

Open the up.sql and down.sql files in migrations folder and add with following sql respectively.

--migrations/TIMESTAMP_users/up.sql
CREATE TABLE users (
  email VARCHAR(100) NOT NULL UNIQUE PRIMARY KEY,
  hash VARCHAR(122) NOT NULL, --argon hash
  created_at TIMESTAMP NOT NULL
);

--migrations/TIMESTAMP_users/down.sql
DROP TABLE users;

--migrations/TIMESTAMP_invitations/up.sql
CREATE TABLE invitations (
  id UUID NOT NULL PRIMARY KEY,
  email VARCHAR(100) NOT NULL,
  expires_at TIMESTAMP NOT NULL
);

--migrations/TIMESTAMP_invitations/down.sql
DROP TABLE invitations;
Enter fullscreen mode Exit fullscreen mode

Migrations allow us to evolve the database schema over time. Each migration can be applied (up.sql) or reverted (down.sql). Applying and immediately reverting a migration should leave your database schema unchanged.
Use the command diesel migration run to apply changes made to the database and in doing so creates a file src/schema.rs that acts as a communication channel for making queries to the database.

create rust equivalent structs to SQL database tables

// src/model.rs
use super::schema::*;

#[derive(Insertable, Queryable)]
#[table_name = "users"]
pub struct User {
  pub email: String,  // VARCHAR(100)
  pub hash: String,  // VARCHAR(122)
  pub created_at: chrono::NativeDateTime  // TIMESTAMP
}

#[derive(Insertable, Queryable)]
#[table_name = "invitations"]
pub struct Invitation {
  pub id: uuid::Uuid,  // UUID
  pub email: String,  // VARCHAR(100)
  pub expires_at: chrono::NativeDateTime  // TIMESTAMP
}
Enter fullscreen mode Exit fullscreen mode

On the second line an import statement use super::schema::*; which will be explained very shortly but for now answering the question
How does diesel use these struct to send & receive data? Take priority and that answer lies in the attributes atop each struct adding that functionality. The first #[derive(Insertable, Queryable)] is know as the derive attibute, then there's #[table_name = "users"] custom to diesel and required by the Insertable trait.

Expanding further on the modularity of Rust, ignore all other mod declearations and focus on the ones that contain code, you can visualize modules in a scope like so.

// src/main.rs
#[macro_use]
extern crate diesel;

use diesel::prelude::*;

// mod attendance_handler;
// mod errors;
// mod invitation_handler;
mod models{
  use super::schema::*;

  #[derive(Insertable, Queryable)]
  #[table_name = "users"]
  pub struct User {
    pub email: String,  // VARCHAR(100)
    pub hash: String,  // VARCHAR(122)
    pub created_at: chrono::NativeDateTime  // TIMESTAMP
  }

  #[derive(Insertable, Queryable)]
  #[table_name = "invitations"]
  pub struct Invitation {
    pub id: uuid::Uuid,  // UUID
    pub email: String,  // VARCHAR(100)
    pub expires_at: chrono::NativeDateTime  // TIMESTAMP
  }
}

// mod register_handler;
mod schema{
  table! {
    invitations (id) {
        id -> Uuid,
        email -> Varchar,
        expires_at -> Timestamp,
    }
  }

  table! {
    users (email) {
        email -> Varchar,
        hash -> Varchar,
        created_at -> Timestamp,
    }
  }

  allow_tables_to_appear_in_same_query!(
    invitations,
    users,
  );
}
// mod utils;

...
Enter fullscreen mode Exit fullscreen mode

First of is the #[macro_use] attribute atop extern crate diesel; which simply imports the macros from the external diesel crate and remember the schema files has macros that aren't defaults in Rust so table! & allow_tables_to_appear_in_same_query! won't work unless this declaration exists and not just anywhere, it has to be in the parent scope.

The import statement in the mod deceleration for src/models.rs use::super::schema::*;.
The use keyword is a declaration, super refers to the parent scope (outside of mod model), schema is the path to mod schema and * simply says port everything declared in mod schema over to mod models. The string literals in the table_name attribute represent tables in mod schema which diesel uses to communicate back and fort from DB tables to Rust.
Lastly use diesel::prelude::* which imports the most important traits and types defined by diesel.
P.S. this won't be in the final code it's just for explaining the modularity of Rust.

updating cargo.toml to include diesel

[package]
name = "rsvp"
version = "0.1.0"
edition = "2018"

[dependencies]
diesel = { version = "1.4.4", features = ["postgres", "uuidv07", "r2d2", "chrono"] }
Enter fullscreen mode Exit fullscreen mode

Resources

Repo

Discussion (2)

Collapse
ecj222 profile image
Enoch Chejieh

This is really good!

Collapse
doordashcon profile image
Doordashcon Author

Thank you!