Rust Web API

From bibbleWiki
Revision as of 23:18, 9 January 2025 by Iwiseman (talk | contribs) (actix_web)
Jump to navigation Jump to search

Introduction

This page is to point me in the right direction should someone want to do this in rust. I think that Golang, C# or NodeJS (to reduce technology) might be a better choice. This language seems a lot harder to learn. Maybe if you already use it for other stuff in you company. Doing this I was surprise how easy it was. Maybe microcontrollers have clouded my judgement

Server

For the server I am going to use actix_web. I am sure there are many choices knowing rust but this is the first one I came across. May rewrite this as we go but here we are

    HttpServer::new(move || {
        App::new()
            .app_data(repository::AppState::new(db.clone()))
            .wrap(Logger::default())
            .configure(routes::home_routes::config) // Better Approach
            .service(hello) // Sample route 1
            .service(echo)  // Sample route 2 
            .route("/hey", web::get().to(manual_hello))
    })
    .bind((address, port))?
    .run()
    .await

Routes

Here is the example for the routes. We will need to look at middleware no doubt

pub fn config(config: &mut web::ServiceConfig) {
    config.service(
        web::scope("/home")
            .service(handlers::home_handlers::greet)
            .service(handlers::home_handlers::test),
    );
}

Handlers

And here are the handlers which I will hopefully update to have your 401, 404 etc

use actix_web::{get, web, Responder};

#[get("/greet/{name}")]
pub async fn greet(name: web::Path<String>) -> impl Responder {
    format!("Hello {name}!",)
}

#[get("/test")]
pub async fn test() -> impl Responder {
    format!("Hello world!")
}

Database

Setup

Well this is only meant to be brief as there are many ways to solve this. Here we are with the database bit. They use sea-orm, and you know how I feel about orms. So for postgress we add

sea-orm = { version = "1.1.3", features = [
    "sqlx-postgres",
    "runtime-tokio-rustls",
    "macros",
] }

Create a database and user. Then you can make a connection. Note migrations rely on DATABASE_URL so you may want to consider this. I have put the Migrator script to run before creating the server. We will need to complete migration before this will run again

    let database_url = format!(
        "postgres://{}:{}@{}:{}/{}",
        std::env::var("DB_USER").unwrap(),
        std::env::var("DB_PASSWORD").unwrap(),
        std::env::var("DB_HOST").unwrap(),
        std::env::var("DB_PORT").unwrap(),
        std::env::var("DB_NAME").unwrap(),
    );

    let db = Database::connect(&database_url).await.unwrap();

    Migrator::up(&db, None).await.unwrap();

Migration

We can now use the sea-orm-cli to make a migration script

cargo install sea-orm-cli
sea-orm-cli migrate init
sea-orm-cli migrate generate create_user_table

The first command creates a separate project which we can create out migration scripts in. And the second one adds a script for our user table we amend this to our needs

use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .create_table(
                Table::create()
                    .table(User::Table)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(User::Id)
                            .integer()
                            .not_null()
                            .auto_increment()
                            .primary_key(),
                    )
                    .col(ColumnDef::new(User::Name).string().not_null())
                    .col(ColumnDef::new(User::Email).string().not_null().unique_key())
                    .col(ColumnDef::new(User::Password).string().not_null())
                    .to_owned(),
            )
            .await
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .drop_table(Table::drop().table(User::Table).to_owned())
            .await
    }
}

#[derive(DeriveIden)]
pub enum User {
    Table,
    Id,
    Name,
    Email,
    Password,
}

You will need to change the migration Cargo.toml so have the relevant drivers and then you can run the migration

sea-orm-cli migrate up

You can use up for migrating forward, down for backward and others see [options]

Entities

We can now generate the entities we want to use in our code. This is done by the sea-orm-cli. I did find it a bit nasty that you have to create the Cargo.toml manually afterwards

sea-orm-cli generate entity -o entity/src

For the Cargo.toml we just add serde and sea-orm

[package]
name = "entity"
version = "0.1.0"
edition = "2021"
publish = false

[lib]
name = "entity"
path = "src/mod.rs"

[dependencies]
serde = { verison = "1.0.197", features = [ "derive" ] }

[dependencies.sea-orm]
version = "1.1.3"

Project Cargo Changes

The migration and the entities are libraries so we need to reference them in the root Cargo project we do this with the workspace and adding them as a dependency

...
[workspace]
members = [".", "entity", "migration"]

[dependencies]
entity = { path = "entity" }
migration = { path = "migration" } # depends on your needs

sea-orm = { version = "1.1.3", features = [
    "sqlx-postgres",
    "runtime-tokio-rustls",
    "macros",
] }
...