Production-ready microservice in Rust: 1. Setup the workspace

Posted 2023-05-07 14:00:00 by sanyi ‐ 3 min read

Setup a multi-package workspace for our application

For complex Rust applications I prefer to use a workspace with multiple packages. This way we can split our application into smaller, loosely coupled parts but we don't have to maintain a different repository for each of them and we can work with the whole codebase in one IDE window.

To start our new project, let's create a directory to hold the workspace:

$ mkdir shelter-project
$ cd shelter-project

and create a Cargo.toml file there:

[workspace]

members = [
  "shelter_main"
]

Also initialize a git repository, to prevent cargo new from creating a new repository for every package:

$ git init

I use JetBrains CLion with the Rust plugin so I usually add a .gitignore file to ignore the .idea folder and the Rust target folder:

.idea
target

Let's create the shelter_main package:

$ cargo new shelter_main

Now our directory structure looks like this:

.git
.gitignore
Cargo.toml
shelter_main/
  Cargo.toml
  src/
    main.rs

We can run cargo build in the main workspace directory, the build creates an executable in target/debug/shelter_main and a single Cargo.lock file in the main workspace directory.

Now we can commit our code:

$ git add .gitignore Cargo.lock  Cargo.toml  shelter_main/
$ git commit -m 'workspace setup'

I usually design my Rust application to handle multiple CLI commands. For example, a shelter_main serve command to run the REST application server, a shelter_main migrate to run the database migrations, etc.

For command-line argument processing I usually use the clap crate, we can add it to the shelter_main package:

$ cargo add clap@4

Let's modify shelter_main/src/main.rs:

use clap::{Arg, Command};

pub fn main() {
    let command = Command::new("Dog Shelter sample application")
        .version("1.0")
        .author("Sandor Apati <[email protected]>")
        .about("A sample application to experiment with Rust-based microservices")
        .arg(
            Arg::new("config")
                .short('c')
                .long("config")
                .help("Configuration file location")
                .default_value("config.json"),
        );

    let _matches = command.get_matches();
}

This creates a simple parameter named -c or --config to set the configuration file location for our application.

We can test is easily:

$ cargo build
$ ./target/debug/shelter_main --help

A sample application to experiment with Rust-based microservices

Usage: shelter_main [OPTIONS]

Options:
  -c, --config <config>  Configuration file location [default: config.json]
  -h, --help             Print help
  -V, --version          Print version

A few more crates I usually add to the project:

$ cargo add [email protected]
$ cargo add anyhow@1

The dotenv crate supports the loading of environment variables from .env files. The anyhow crate simplifies error handling in Rust - it will be important later.

Modify our main.rs to use them:

use clap::{Arg, Command};
use dotenv::dotenv;

pub fn main() -> anyhow::Result<()> {
    dotenv().ok();

    let command = Command::new("Dog Shelter sample application")
        .version("1.0")
        .author("Sandor Apati <[email protected]>")
        .about("A sample application to experiment with Rust-based microservices")
        .arg(
            Arg::new("config")
                .short('c')
                .long("config")
                .help("Configuration file location")
                .default_value("config.json"),
        );

    let _matches = command.get_matches();

    Ok(())
}

The next post will be about the implementation of the CLI sub-commands.

You can find the sample code on GitHub

Next article ยป

Tags:
rust microservice dog-shelter