Rust Workspaces: A guide to managing your code better

In this guide, you will learn what Rust workspaces are, their benefits, and a practical guide on how to build a REST API using Rust workspaces.

avatar

Demola Malomo

Aug 06 2024

6 min read

avatar

A common paradigm in most programming languages is the use of files, modules, folders, and packages to break down codebases into separate and organized segments. Well, Rust isn’t an exception to this. You can structure your code in files and modules to achieve the desired result.

While files and modules work, they come with it’s limitations, as you can only use the separated code in a single codebase. An approach Rust uses to make your code reusable across multiple projects is the use of workspaces.

In this guide, you will learn what Rust workspaces are, their benefits, and a practical guide on how to build a REST API using Rust workspaces.

What are Rust Workspaces?

Rust workspace is a feature that lets you manage multiple related packages in one place. They let you structure your code for code reuse, simplify development workflow, and improve the overall developer experience.

Benefits of Rust Workspaces:

  1. Code reusability: Workspaces allow you to share your code among multiple projects, which makes it easier to maintain and update.

  2. Simplified dependency management: With workspaces, you can manage dependencies centrally for all member crates, avoid duplication, and ensure consistency.

  3. Improved organization: Keep related projects together so that you can easily navigate and understand the codebase.

  4. Streamlined builds: It lets you build all crates in the workspace simultaneously, which reduces build times and simplifies CI/CD processes.

In our case, you will use Rust Workspaces to build a REST API with Xata, a serverless data platform built on top of PostgreSQL for creating modern and robust applications.

Workspace showcase

The first step in creating workspaces is to architect your application by categorizing components into those that can be reused in multiple projects and those that are specific to a particular project. In this case, the data model and database service can be structured using workspaces, while the API handlers can leverage these workspaces.

Prerequisites

To follow along with this tutorial, you’ll need to have:

  • Basic understanding of Rust
  • Xata account. Signup is free
  • Postman or any API testing application of your choice

Scaffold the project

To get started, navigate to the desired directory and run the command below:

1cargo new rust-worskpace && cd rust-worskpace

This command creates a Rust project called rust-workspace and navigates into the project directory.

Next, install the required dependency by modifying the [dependencies] section of the Cargo.toml file as shown below:

1[dependencies] 2actix-web = "4.4.1"

actix-web = "4.4.1" is a Rust-based framework for building web applications.

Setup the database

Log into your Xata workspace and create a project database with the following columns:

Column typeColumn name
Stringname
Textdescription
Stringstatus

database

Next, click the Get code snippet button to get the database URL and navigate to your workspace settings to generate an API key.

Get URL

Generate API Key

Finally, create .env file in the root directory and add the copied URL and API key.

1XATA_DATABASE_URL= <REPLACE WITH THE COPIED DATABASE URL> 2XATA_API_KEY=<REPLACE WITH THE COPIED API KEY>

With this done, you can now create the required workspaces that the application will use.

Data model workspace

To get started, run the command below:

1cargo new --lib shared_models

The command above will create a library called shared_models.

Next, install the required dependency by modifying the [dependencies] section of the shared_models/Cargo.toml file as shown below:

1#shared_models/Cargo.toml 2 3[dependencies] 4serde = { version = "1.0.195", features = ["derive"] }

Finally, update the shared_models/src/lib.rs file as shown below:

1use serde::{Deserialize, Serialize}; 2 3#[derive(Deserialize, Serialize, Debug, Clone)] 4pub struct Project { 5 pub id: String, 6 pub name: String, 7 pub description: String, 8 pub status: String, 9} 10 11#[derive(Deserialize, Serialize, Debug, Clone)] 12pub struct ResponseData { 13 pub records: Vec<Project>, 14} 15 16#[derive(Deserialize, Serialize, Debug, Clone)] 17pub struct ProjectRequest { 18 pub name: String, 19 pub description: String, 20 pub status: String, 21} 22 23#[derive(Deserialize, Serialize, Debug, Clone)] 24pub struct ProjectResponse { 25 pub id: String, 26} 27 28#[derive(Serialize, Debug, Clone)] 29pub struct APIResponse<T> { 30 pub status: u16, 31 pub message: String, 32 pub data: Option<T>, 33} 34 35#[derive(Serialize, Debug, Clone)] 36pub struct APIErrorResponse { 37 pub status: u16, 38 pub message: String, 39 pub data: Option<String>, 40}

The snippet above does the following:

  • Imports the required dependency.
  • Creates a Project, ResponseData, ProjectRequest, and ProjectResponse structs with required properties to describe the request and response body accordingly.
  • Creates an APIResponse, and APIErrorResponse structs with the required properties needed for the API response

Database service workspace

To get started, run the command below:

1cargo new --lib xata_client

The command above will create a library called xata_client.

Next, install the required dependency by modifying the [dependencies] section of the xata_client/Cargo.toml file as shown below:

1#xata_client/Cargo.toml 2 3[dependencies] 4dotenv = "0.15.0" 5reqwest = { version = "0.11.23", features = ["json"] } 6serde_json = "1.0.111" 7shared_models = { path = "../shared_models" }

dotenv = "0.15.0" is a library for loading environment variables.

reqwest = { version = "0.11", features = ["json"] } for making HTTP requests.

serde_json = "1.0" for manipulating JSON data.

shared_models = { path = "../shared_models" } adds the model workspace you created earlier to xata_client library. By doing this, you’ll be able to use the shared_models workspace in this newly created workspace.

Finally, update the xata_client/src/lib.rs file as shown below:

1use std::env; 2 3use dotenv::dotenv; 4use reqwest::{header, Client, Error}; 5use shared_models::{Project, ProjectRequest, ProjectResponse, ResponseData}; 6 7pub struct XataClient; 8 9impl XataClient { 10 fn env_loader(key: &str) -> String { 11 dotenv().ok(); 12 match env::var(key) { 13 Ok(v) => v.to_string(), 14 Err(_) => format!("Error loading environment variable"), 15 } 16 } 17 18 fn init() -> Client { 19 Client::new() 20 } 21 22 fn create_header() -> header::HeaderMap { 23 let mut headers = header::HeaderMap::new(); 24 headers.insert("Content-Type", "application/json".parse().unwrap()); 25 headers.insert( 26 header::AUTHORIZATION, 27 format!("Bearer {}", XataClient::env_loader("XATA_API_KEY")) 28 .parse() 29 .unwrap(), 30 ); 31 headers 32 } 33 34 pub async fn create_project(new_project: ProjectRequest) -> Result<ProjectResponse, Error> { 35 let database_url = XataClient::env_loader("XATA_DATABASE_URL"); 36 let url = format!("{}:main/tables/Project/data", database_url); 37 let client = XataClient::init() 38 .post(url) 39 .headers(XataClient::create_header()) 40 .json(&new_project) 41 .send() 42 .await; 43 match client { 44 Ok(response) => { 45 let json = response.text().await?; 46 let created_project: ProjectResponse = serde_json::from_str(json.as_str()).unwrap(); 47 Ok(created_project) 48 } 49 Err(error) => Err(error), 50 } 51 } 52 53 pub async fn get_projects() -> Result<Vec<Project>, Error> { 54 let database_url = XataClient::env_loader("XATA_DATABASE_URL"); 55 let url = format!("{}:main/tables/Project/query", database_url); 56 let client = XataClient::init() 57 .post(url) 58 .headers(XataClient::create_header()) 59 .send() 60 .await; 61 match client { 62 Ok(response) => { 63 let json = response.text().await?; 64 let response_data: ResponseData = serde_json::from_str(json.as_str()).unwrap(); 65 Ok(response_data.records) 66 } 67 Err(error) => Err(error), 68 } 69 } 70}

The snippet above does the following:

  • Imports the required dependencies
  • Creates an XataClient struct
  • Creates an implementation block that adds env_loader, init, create_header, create_project, and get_projects methods. These methods are designed to load environment variables, create a connection pool for making asynchronous requests, generate request headers, create a project, and get the list of products.

Building the APIs with the workspaces

With the workspaces fully set up, you might think you can now start using the libraries in the main project. Well, not so fast—you need to specify them as dependencies in the main project first.

While Cargo automatically recognizes the libraries as part of the workspace, you still have to explicitly add them as dependencies. If you check the Cargo.toml file in the root directory, you’ll see the workspaces added as shows below:

1workspace = { members = ["shared_models", "xata_client"] }

To add the libraries as depencies, update the [dependencies] section by specifying the library and the associated path.

1#rust-workspace/Cargo.toml 2 3[dependencies] 4actix-web = "4.4.1" 5#add workspaces below 6shared_models = { path = "./shared_models" } 7xata_client = { path = "./xata_client" }

With that done, you can now use the library to build the APIs.

To do this, create a handler.rs file inside the src folder of the project and add the snippet below:

1use actix_web::{http::StatusCode, post, web::Json, HttpResponse}; 2use shared_models::{APIErrorResponse, APIResponse, Project, ProjectRequest, ProjectResponse}; 3use xata_client::XataClient; 4 5#[post("/project")] 6pub async fn create_project_handler(new_project: Json<ProjectRequest>) -> HttpResponse { 7 let create_project = XataClient::create_project(new_project.to_owned()).await; 8 9 match create_project { 10 Ok(data) => HttpResponse::Created().json(APIResponse::<ProjectResponse> { 11 status: StatusCode::CREATED.as_u16(), 12 message: "success".to_string(), 13 data: Some(data), 14 }), 15 Err(error) => HttpResponse::InternalServerError().json(APIErrorResponse { 16 status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), 17 message: "failure".to_string(), 18 data: Some(error.to_string()), 19 }), 20 } 21} 22 23#[post("/projects")] 24pub async fn get_projects_handler() -> HttpResponse { 25 let get_projects = XataClient::get_projects().await; 26 27 match get_projects { 28 Ok(data) => HttpResponse::Ok().json(APIResponse::<Vec<Project>> { 29 status: StatusCode::OK.as_u16(), 30 message: "success".to_string(), 31 data: Some(data), 32 }), 33 Err(error) => HttpResponse::InternalServerError().json(APIErrorResponse { 34 status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), 35 message: "failure".to_string(), 36 data: Some(error.to_string()), 37 }), 38 } 39}

The snippet above does the following:

  • Imports the required dependencies (including the workspaces)
  • Creates a create_project_handler and get_projects_handler handler with corresponding API routes that use the libraries to perform the corresponding actions and return the appropriate response using the APIResponse and APIErrorResponse

Putting it all together

With that done, update the main.rs file to include the application entry point and use the handlers.

1use actix_web::{App, HttpServer}; 2use handlers::{create_project_handler, get_projects_handler}; 3mod handlers; 4 5#[actix_web::main] 6async fn main() -> std::io::Result<()> { 7 HttpServer::new(move || { 8 App::new() 9 .service(create_project_handler) 10 .service(get_projects_handler) 11 }) 12 .bind(("localhost", 8080))? 13 .run() 14 .await 15}

The snippet above does the following:

  • Imports the required dependencies
  • Creates a new server that adds the handlers and runs on localhost:8080

With that done, we can start a development server using the command below:

1cargo run

API test

This post discussed what Rust workspaces are, their benefits, and a walkthrough guide on how to use them to build a REST API with Xata. Beyond what was explored above, you can extend the workspaces and the API to support deleting, editing, and getting a single project.

These resources may be helpful:

Related posts