Zero To Production In Rust

repository·main·Indexed 27 days ago

https://github.com/lukemathwalker/zero-to-production

An opinionated introduction to backend development using Rust, featuring codebase snapshots for an email newsletter project. The project includes implementations for authentication using Argon2id, a layered configuration system, idempotency management with Postgres, and subscription workflows using Actix-web and sqlx.

Tokens
3.8K
Snippets
14
Records
24
Agent score
92%

What's inside zero2prod

  1. Install pre-requisites for Zero To Production In Rust

    main

    To use this project, you must have Rust and Docker installed. Depending on your operating system, additional tools are required to support the build and database processes.

    ### Windows
    
    ```bash
    cargo install -f cargo-binutils
    rustup component add llvm-tools-preview
    cargo install --version="~0.7" sqlx-cli --no-default-features --features rustls,postgres

    Linux

    # Ubuntu 
    sudo apt-get install lld clang libssl-dev postgresql-client
    # Arch 
    sudo pacman -S lld clang postgresql
    cargo install --version="~0.7" sqlx-cli --no-default-features --features rustls,postgres

    MacOS

    brew install michaeleisel/zld/zld
    cargo install --version="~0.7" sqlx-cli --no-default-features --features rustls,postgres
  2. Build and run the project

    main

    Follow these steps to initialize the required infrastructure (Postgres and Redis), build the application, and run the web server.

    1. Initialize the Postgres database using Docker.
    2. Initialize the Redis instance using Docker.
    3. Build the project using cargo.
    4. Run the server using cargo run.

    Once running, you can access the application at http://127.0.0.1:8000/login.

    Default Credentials:

    • Username: admin
    • Password: everythinghastostartsomewhere
    ./scripts/init_db.sh
    ./scripts/init_redis.sh
    cargo build
    cargo run
  3. Run the zero2prod application

    main

    The main function serves as the entrypoint for the zero2prod application. It initializes telemetry, loads configuration, and concurrently runs two primary tasks: the API server and the background issue delivery worker. The application uses tokio::select! to monitor these tasks and will report an error and exit if either the API or the background worker fails or completes unexpectedly.

    #[tokio::main]
    async fn main() -> anyhow::Result<()> {
        let subscriber = get_subscriber("zero2prod".into(), "info".into(), std::io::stdout);
        init_subscriber(subscriber);
    
        let configuration = get_configuration().expect("Failed to read configuration.");
        let application = Application::build(configuration.clone()).await?;
        
        let application_task = tokio::spawn(application.run_until_stopped());
        let worker_task = tokio::spawn(run_worker_until_stopped(configuration));
    
        tokio::select! {
            o = application_task => report_exit("API", o),
            o = worker_task =>  report_exit("Background worker", o),
        };
    
        Ok(())
    }
  4. Load application configuration using get_configuration()

    main

    The application uses a layered configuration system. It loads settings from a base.yaml file, an environment-specific YAML file (e.g., local.yaml or production.yaml), and environment variables.

    To determine which environment file to load, set the APP_ENVIRONMENT environment variable. If unset, it defaults to local.

    Configuration Hierarchy:

    1. configuration/base.yaml
    2. configuration/{APP_ENVIRONMENT}.yaml
    3. Environment variables prefixed with APP__ (using __ as a separator for nested structures).
  5. Subscribe a new user via the subscribe handler

    main

    The subscribe function is an Actix-web route handler used to process new newsletter subscriptions. It accepts subscription data via a form, inserts the subscriber into the database with a pending_confirmation status, generates and stores a unique 25-character alphanumeric subscription token, and sends a confirmation email.

    Error Responses

    • 400 Bad Request: Returned if the provided email or name fails validation (SubscribeError::ValidationError).
    • 500 Internal Server Error: Returned for any unexpected database or email client failures (SubscribeError::UnexpectedError).
    pub async fn subscribe(
        form: web::Form<FormData>,
        pool: web::Data<PgPool>,
        email_client: web::Data<EmailClient>,
        base_url: web::Data<ApplicationBaseUrl>,
    ) -> Result<HttpResponse, SubscribeError>
  6. Send a confirmation email to a new subscriber

    main

    The send_confirmation_email function constructs and sends a subscription confirmation email using the provided EmailClient. It generates both a plain text and an HTML version of the email, containing a link to the /subscriptions/confirm endpoint with the subscription_token as a query parameter.

    Arguments

    • email_client: A reference to the EmailClient used to dispatch the email.
    • new_subscriber: The NewSubscriber domain object containing the recipient's email.
    • base_url: The base URL of the application (e.g., https://example.com) used to construct the confirmation link.
    • subscription_token: The unique token generated for this subscription.
    pub async fn send_confirmation_email(
        email_client: &EmailClient,
        new_subscriber: NewSubscriber,
        base_url: &str,
        subscription_token: &str,
    ) -> Result<(), reqwest::Error>