Zero to Production In Rust, Chapter 1

For 2024, I want to embrace Rust as a backend platform. I have used it professionally for a proxy-wasm Envoy TCP filter. I had to parse a binary protocol. The nom crate made chopping up the TCP packets quite nice. The resulting parsing code ended up resembling the BNF grammar for the protocol. The parser functions mapped to the symbols defined in the grammar. A coworker who has little experience with Rust found the nom parsers easy to understand.

Rust also proved to be valuable in modeling the Finite-state machine of the protocol. The TypeState pattern made state transitions explicit in the type system. Rust's algebraic data types made the states explicit as well.

To go further into Ferris' snappy embrace, I am going to read the book, Zero To Production In Rust, by Luca Palmieri.

Zero to Production

Last year at some point I came across the book, Zero To Production In Rust. I was blown away by how comprehensive it is. It walks you through building a backend service in Rust. I have written many backend services in my 20+ years as a software engineer, and Luca covers everything you need. A junior engineer can learn a lot from this book.

It starts you from an empty git repo. He sets you up with a CI/CD pipeline. He walks you through developing user stories for the service. Not only that, but he even shows you how to set up telemetry for the service.

Today I will read through Chapter 1 and document my thoughts as I go. Every week I plan on doing another chapter. We will see how this goes.

Chapter 1

Chapter 1 sets up my development environment. First, Luca talks about the different release channels for the Rust compiler. There are three release channels, stable, beta, and nightly.

When I got started with Rust, it was common for packages to require nightly. These days, unless you're on the cutting edge of the Rust ecosystem, stable is recommended.

Project setup

Luca suggests to use rustup to install Rust. This is good advice, but I am a weirdo and I use nix. I will be using nix flakes rather than rustup to manage my tools. The benefit of using a nix flake over the typical rustup method is that all my project's build dependencies are explicit. nix makes my project predicable for all time and space.

I start by running nix flake new.

This creates the nix files for a Rust project. I'm using direnv to configure my shell. The Rust template provides all the basic Rust tools: Cargo, the stable rust compile, rustfmt, and clippy.

Next, I need to initialize the project with Cargo:

~/src/zero2prod ❯ cargo init

~/src/zero2prod main* ❯ ls -a
.  ..  Cargo.toml  default.nix  .direnv  .envrc  flake.lock  flake.nix  .git  .github  .gitignore  shell.nix  src

Cargo automatically creates a git repo for me. This has a strange effect on nix flake. Nix requires that all files are added to the git repo. This ensures that your project is reproducible. It prevents you from building with files that are not version controlled. Unfortunately, this manifests as this strange error message:

~/src/zero2prod main* ❯ direnv reload
direnv: loading ~/src/zero2prod/.envrc                                           direnv: using flake

warning: Git tree '/home/eric/src/zero2prod' is dirty
error: getting status of '/nix/store/0ccnxa25whszw7mgbgyzdm4nqc0zwnm8-source/flake.nix': No such file or directory
error: getting status of '/home/eric/src/zero2prod/.direnv/flake-profile.155235': No such file or directory
warning: Git tree '/home/eric/src/zero2prod' is dirty
error: getting status of '/nix/store/0ccnxa25whszw7mgbgyzdm4nqc0zwnm8-source/flake.nix': No such file or directory
direnv: nix-direnv: renewed cache
direnv: export ~PATH ~XDG_DATA_DIRS

This is easily fixed by adding the files to the git repository.

~/src/zero2prod main* ❯ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        .direnv/
        .envrc
        .github/
        .gitignore
        Cargo.toml
        default.nix
        flake.lock
        flake.nix
        shell.nix
        src/

nothing added to commit but untracked files present (use "git add" to track)

# Ignore .direnv/ because it is local only

~/src/zero2prod main* ❯ echo '.direnv/' >> .gitignore 

# Add the file, no need to commit just yet.

~/src/zero2prod main* ❯ git add .

# Reload to verify that the error is gone.

~/src/zero2prod main* ❯ direnv reload
direnv: loading ~/src/zero2prod/.envrc
direnv: using flake
warning: Git tree '/home/eric/src/zero2prod' is dirty
warning: Git tree '/home/eric/src/zero2prod' is dirty
direnv: nix-direnv: renewed cache
direnv: export +AR +AS +CC +CONFIG_SHELL +CXX +HOST_PATH +IN_NIX_SHELL +LD +NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_BUILD_CORES +NIX_CC +NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_CFLAGS_COMPILE +NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE +NIX_LDFLAGS +NIX_STORE +NM +OBJCOPY +OBJDUMP +RANLIB +READELF +RUST_SRC_PATH +SIZE +SOURCE_DATE_EPOCH +STRINGS +STRIP +__structuredAttrs +buildInputs +buildPhase +builder +cmakeFlags +configureFlags +depsBuildBuild +depsBuildBuildPropagated +depsBuildTarget +depsBuildTargetPropagated +depsHostHost +depsHostHostPropagated +depsTargetTarget +depsTargetTargetPropagated +doCheck +doInstallCheck +dontAddDisableDepTrack +mesonFlags +name +nativeBuildInputs +out +outputs +patches +phases +preferLocalBuild +propagatedBuildInputs +propagatedNativeBuildInputs +shell +shellHook +stdenv +strictDeps +system ~PATH ~XDG_DATA_DIRS

~/src/zero2prod main* ❯ cargo --version
cargo 1.74.0

IDE setup

Luca recommends intellij-rust. If you're a Jetbrains fan and have money to burn, go for it. However, as I said before, I am a weirdo. I use Emacs. You could also use VS Code if you're so inclined.

Emacs has lsp-mode to turn Emacs into a fancy pants IDE. In order to use Rust with lsp-mode, I need to add rust-analyzer to my dev shell:

I use the envrc Emacs package to manage my Emacs project's environment with direnv. After I changed my flake.nix I run M-x envrc-reload and rust-analyzer is available to lsp-mode.

CI/CD

Luca is very in-depth with is continuous integration steps. He requires tests to pass, code coverage to be collected, linting to pass, code to be formatted, and the code to pass security audits.

These checks are the bare minimum requirements for a professional software project. He details how to install the tools manually. I can skip his instructions because nix provides them. I just have to add them to my flake.nix.

Finally, for the time being, I'm going to just use a Makefile to run my checks. I'll integrate with GitLab CI later.

check:
        cargo test
        cargo tarpaulin --ignore-tests
        cargo clippy -- -D warnings
        cargo fmt -- --check
        cargo audit

M-x envrc-reload reloads my environment with the new tools. projectile-compile-project (C-c p c) with make check will run my checks.

Having all these tools managed by a flake.nix file makes it easy to come back to this project in the future. I don't have to look up how to install Rust, tarpaulin, or cargo-audit. All that tooling is ready for me as soon as I cd into the directory or open the project in Emacs.

End of Chapter 1

OK, that is the start of my project. I've configured nix to manage all my development dependencies. I've set up a basic CI/CD pipeline. Next week I will continue with Chapter 2, Building An Email Newsletter.