DEV Community

Nivethan
Nivethan

Posted on • Updated on • Originally published at nivethan.dev

A Web App in Rust - 06 Registering a User

Ok! The previous chapter was all set up for this chapter and the next few. We now have a database and three tables. We also have a website that can talk to our rust application. We are now going to marry the two in this chapter!

Before we get started we need to add some new crates we're going to be using in our application. We will be adding the dotenv crate and the diesel crate.

./Cargo.toml

[dependencies]
actix-web = "3"
tera = "1.5.0"
serde = "1.0"
dotenv = "0.15.0"
diesel = { version = "1.4.4", features = ["postgres"] }
Enter fullscreen mode Exit fullscreen mode

We will be using the dotenv crate to read our .env file and place it in the environment as environment variables. We will also include diesel proper now. For diesel we want to specify that we are using postgres so we include it via the features option.

With that we have our dependencies taken care of. Let's move on to the fun part!

  • Just a note, make sure that cargo watch did indeed pull in our dependencies, this may require you to kill cargo watch and restart it as I was getting strange errors because cargo watch hadn't picked up the new dependencies.

For now we're going to focus on getting the core functions done. Lets outline what we're going to be doing for the next few chapters.

  1. We should be able to register new users.
  2. We should be able to login.
  3. We should be able to submit new posts.
  4. We should be able to view posts.
  5. We should be able to make comments on each post.

In this chapter we will focus on just connecting to our database and building our user registration process.

Connecting to Our Database

Before we get into the code, lets orient ourselves.

We currently have a schema.rs file in our src directory.

./src/schema.rs

...
table! {
    users (id) {
        id -> Int4,
        username -> Varchar,
        password -> Varchar,
        email -> Varchar,
    }
}
...
Enter fullscreen mode Exit fullscreen mode

This is an automatically generated file by diesel that we will be using throughout our application. One thing to keep in mind at all times is that the schema is the source of truth, the order of the fields and their types needed to be reflected in any structs we make with the trait "Queryable".

The first step is make our application aware of this file.

./src/main.rs

#[macro_use]
extern crate diesel;
pub mod schema;

use actix_web::{HttpServer, App, web, HttpResponse, Responder};
...
Enter fullscreen mode Exit fullscreen mode

We will need the first 3 lines of our main.rs to look like the above. I'm not entirely sure why we need this besides that it doesn't work without it. From what I can tell the macro is allowing the schema.rs file to actually generate in our binary. The extern I have no idea because from my understanding use supersedes it now. Using "use diesel::*" causes the macros in schema.rs to not get created properly. So we'll hand wave it away :)

Now we will write the connector, this is what we will call anytime we want to connect to our hackerclone database and do something with it.

...
use diesel::prelude::*;
use diesel::pg::PgConnection;
use dotenv::dotenv;

fn establish_connection() -> PgConnection {
    dotenv().ok();

    let database_url = std::env::var("DATABASE_URL")
        .expect("DATABASE_URL must be set");

    PgConnection::establish(&database_url)
        .expect(&format!("Error connecting to {}", database_url))
}
...
Enter fullscreen mode Exit fullscreen mode

We include some parts of diesel and we also include the dotenv crate. We start by making sure that our environment file is properly set up and then we read in the database url from the environment. the dotenv package would have done this.

We then return a connection handler with the type PgConnection.

! We can now connect to our database!

Registering New Users

Finally! The funner part. We had created a User struct in one of the previous chapters to us to extract the User data from a request. Now we will use that same struct but with some minor modifications. This will also be where we start branching into different files. We could technically do this all in main.rs but then it would be quite unwieldy.

The first step to processing new users is to create a models.rs file and move our User struct to there.

./src/models.rs

use super::schema::users;
use diesel::{Queryable, Insertable};
use serde::Deserialize;

#[derive(Queryable)]
pub struct User {
    pub id: i32,
    pub username: String,
    pub email: String,
    pub password: String,
}

#[derive(Debug, Deserialize, Insertable)]
#[table_name="users"]
pub struct NewUser {
    pub username: String,
    pub email: String,
    pub password: String,
}
Enter fullscreen mode Exit fullscreen mode

I copied over our old struct and renamed it NewUser. This is because we need two versions when want to interact with the User table. This will be true for most tables.

The NewUser struct corresponds to a user that we will extract from a request and will put into the User table. This is why it derives the Insertable trait.

The User struct corresponds to existing users, it's almost like extracting a full user from the database. This user will have the extra parameter of id. This struct has the Queryable trait because we want to be able to query the User table and get everything structured using the User struct.

  • Note here - Because the struct has the Queryable trait, we need to make sure the order and types matches what's in the schema. This resulted in more than one bug for myself!

The next thing to notice is that here we use the schema.rs file, we use it via the super option because the models.rs file is under the root, main.rs file.

We are exposing our structs to other parts of our application through the pub keyword. We can also keep things private if we need to.

Now we have our User models set up! Let's go back our main.rs file.

./src/main.rs

...
pub mod schema;
pub mod models;
...

...
use diesel::prelude::*;
use diesel::pg::PgConnection;
use dotenv::dotenv;

use models::{User, NewUser};
...
Enter fullscreen mode Exit fullscreen mode

The first thing we need to do is expose our models file and also include it. The first statement, "pub mod models" will expose our models file. The use statement is what actually includes the models into our scope.

Now we will update our process_signup function to use our database connector and models.

./src/main.rs

async fn process_signup(data: web::Form<NewUser>) -> impl Responder {
    use schema::users;

    let connection = establish_connection();

    diesel::insert_into(users::table)
        .values(&*data)
        .get_result::<User>(&connection)
        .expect("Error registering user.");

    println!("{:?}", data);
    HttpResponse::Ok().body(format!("Successfully saved user: {}", data.username))
}
Enter fullscreen mode Exit fullscreen mode

The first thing we do is change the Form extractor to NewUser. The next thing to notice is our use statement. Here we are bringing in the the code that generated through the macros in the schema file. This is what will let us refer to the users table.

The next thing we is create our connection and then we do our insert.

Before we do, we can do validation on our new user such as making sure the username is unique, the email is valid or the password is strong enough. For now however we will keep things simple and do our insert straight. Duplicate users will cause our UNIQUE constraint we wrote in the sql files to be violated and this will cause rust to panic.

The values line is one line I have no idea about. Without the * there is an error saying the Insertable trait isn't set up properly on the NewUser struct but I don't know what is wrong with it.

The next line is where we execute our insert passing in the connection and casting it to the type of User. The get_result call returns our newly loaded item and we need to cast it properly.

The expect call will trigger rust to panic if there are any errors in inserting.

We can now go to 127.0.0.1:8000/signup and try it out.

NewUser { username: "nivethan", email: "nivethan@example.com", password: "123" }
Enter fullscreen mode Exit fullscreen mode

If we try to register a new user we should see the above line in our terminal.

thread 'actix-rt:worker:3' panicked at 'Error registering user.: DatabaseError(UniqueViolation, "duplicate key value violates unique constraint \"users_username_key\"")', src/main.rs:72:10
Enter fullscreen mode Exit fullscreen mode

If we try to register an existing user we should see rust panicking.

!! Now we have wired up our rust application to our database and have our website's sign up page actually functioning. We could do more complex things such as verifying a user's e-mail or adding invite codes but let's keep it simple for now.

We're slowly building something! Next we'll working on actually logging our new user in.

Discussion (4)

Collapse
lwshang profile image
Linwei Shang

The values line is one line I have no idea about. Without the * there is an error saying the Insertable trait isn't set up properly on the NewUser struct but I don't know what is wrong with it.

data has the type of web::Form<NewUser>. The * there can deference it into NewUser because we have impl<T> Deref for Form<T> (Doc). And the NewUser struct is the type implements Insertable trait required by the values() function.

Collapse
krowemoh profile image
Nivethan Author • Edited on

Ah! Thank you that makes sense.

& to me implies address and * means get the value at the address, would that be a valid way of thinking that data's address contains the NewUser struct? This would mean web::Form is really the address of some struct T.

Or does Deref mean something else in this context.

Collapse
romanlevin profile image
Roman Levin

web::Form wraps NewUser, but it isn't a pointer to a NewUser.

Deref for web::Form is very simple:

impl<T> ops::Deref for Form<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}
Enter fullscreen mode Exit fullscreen mode

The additional & is necessary, because as rustc tells us, it would mean moving the NewUser out, but that's impossible because data still owns it, so borrowing (&) the dereferenced NewUser allows us to send it to values without violating ownership rules.

An alternative would be to do values(data.into_inner()) which moves the NewUser out of the web::Form by consuming it.

By the way, Nivethan, this is a wonderful series. Thank you so much for the effort you put in!

Collapse
knifehand profile image
Matt Hurt

Could you add a section on how to do unit tests for this tutorial series? I'm new to this and have been attempting to think about how to build one for this particular section. Thank you! Love the tutorial!