Appwrite + Rust: Build APIs without technical overhead
Tags: rust, appwrite, baas, api
Published: 2023-08-18 08:21:48

In every stage of the Sofware Development Lifecycle (SDLC), developers must make strategic decisions around databases, authorization, deployment mechanisms, server sizes, storage management, etc. These decisions must be thoroughly assessed as they can significantly impact the application’s development process.

One paradigm developers constantly embrace is [Backend-as-a-Service (BaaS)](https://www.cloudflare.com/en-gb/learning/serverless/glossary/backend-as-a-service-baas/). Baas abstracts the development overhead associated with SDLC and focuses only on the business logic. It provides developers with server-side capabilities like user authentication, database management, cloud storage, etc.

In this post, we will explore leveraging Appwrite as a BaaS by building a project management API in Rust. The API will provide functionalities to create, read, update, and delete a project. The project repository can be found [here](https://github.com/Mr-Malomz/rust-appwrite).

## What is Appwrite?

Appwrite is an open-source backend as a service platform that provides sets of APIs and SDKs for building web, mobile, and backend services. The following are some of the benefits of using Appwrite in any application:

-   Provides a scalable and robust database
-   Realtime functionalities
-   Support for serverless functions
-   Security certificates and encryption
-   Authentication and authorization mechanism

## Prerequisites

To follow along with this tutorial, the following are needed:

-   Basic understanding of Rust
-   Appwrite account. Signup is [free](https://cloud.appwrite.io/register)

## Getting started

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

```bash
cargo new rust-appwrite && cd rust-appwrite
```

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

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

```toml
[dependencies]
actix-web = "4"
serde = { version = "1.0.145", features = ["derive"] }
serde_json = "1.0"
dotenv = "0.15.0"
reqwest = { version = "0.11", features = ["json"] }
```

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

`serde = { version = "1.0.145", features = ["derive"] }` is a framework for serializing and deserializing Rust data structures.

`serde_json = "1.0"` is a [crate](https://crates.io/) that uses the `serde` crate to manipulate JSON and vice versa.

`reqwest = { version = "0.11", features = ["json"] }` is a HTTP request crate.

We need to run the command below to install the dependencies:

```bash
cargo build
```

**Structuring the application**

It is essential to have a good project structure as it makes the codebase maintainable and seamless for anyone to read or manage.

To do this, we need to navigate to the `src` directory and, in this folder, create an `api` folder. In the `api` folder, we also need to create a `mod.rs`, `models.rs`, `services.rs`, and `handlers.rs` files.

![Updated file structure](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691702661937_Screenshot+2023-08-10+at+22.13.57.png)

`mod.rs` is a file for managing application visibility.

`models.rs` is for structuring our application data.

`services.rs` is for abstracting our application logic.

`handlers.rs` is for structuring our APIs.

Next, we need to declare these files as a module by importing them into the `mod.rs` file

    pub mod handlers;
    pub mod models;
    pub mod services;

Finally, we need to register `api` folder as a parent module by importing it into the `main.rs` file as shown below:

```rust
mod api;

fn main() {
    println!("Hello world")
}
```

## Setting up Appwrite

To get started, we need to log into our Appwrite [console](https://cloud.appwrite.io/console), click the **Create project** button, input `api_rust` as the name, and then **Create**.

![Create project](https://paper-attachments.dropboxusercontent.com/s_C9210607DAFF6D80615947ED6BE237716E98BADEF0416B60FACF081B1CAEFB80_1687518026974_Screenshot+2023-06-23+at+11.38.55.png)

**Create a Database, Collection, and Add Attributes**

Appwrite ships a scalable and robust database that we can use in building our project management API. To do this, first, navigate to the **Database** tab, click the **Create database** button, input `project` as the name, and **Create**.

![Create database](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691705236741_Screenshot+2023-08-10+at+23.06.00.png)

Secondly, we need to create a collection for storing our projects. To do this, click the **Create collection** button, input `project_collection` as the name, and then click **Create**.

![Create collection](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691705351204_Screenshot+2023-08-04+at+01.14.37.png)

Lastly, we need to create attributes to represent our database fields. To do this, we need to navigate to the **Attributes** tab and create attributes for each of the values shown below:

| Attribute key | Attribute type | Size | Required |
| ------------- | -------------- | ---- | -------- |
| name          | String         | 250  | YES      |
| description   | String         | 5000 | YES      |

![Create attribute](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691705420699_Screenshot+2023-08-04+at+01.15.49.png)
![create](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691705440090_Screenshot+2023-08-04+at+01.19.11.png)

After creating the attributes, we see them as shown below:

![List of attributes](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691705645782_Screenshot+2023-08-04+at+01.21.03.png)

**Create an API key**

To securely connect to Appwrite, we need to create an API key. To do this, we need to navigate to the **Overview** tab, scroll to the **Integrate With Your Server section**, and click the **API Key** button.

![Create API key](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691709339925_Screenshot+2023-08-04+at+01.37.13.png)

Next, input `api_rust` as the name, click the **Next** button, select **Database** as the required scope, and **Create**.

![input  name create](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691709339959_Screenshot+2023-08-04+at+01.39.23.png)
![Set permission](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691709613147_Screenshot+2023-08-04+at+01.40.32.png)

## Leveraging Appwrite to build the project management APIs in Rust

With our project fully set up on Appwrite, we can now use the database without manually creating a server.

**Set up Environment Variable**

To securely connect to our Appwrite provisioned server, Appwrite provides an endpoint and sets of unique IDs to perform all the required actions. To set up the required environment variables, we need to create a `.env` file in the root directory and add the snippet below:

```bash
API_KEY=<REPLACE WITH API KEY>
PROJECT_ID=<REPLACE WITH PROJECT ID>
DATABASE_ID=<REPLACE WITH DATABASE ID>
COLLECTION_ID=<REPLACE WITH COLLECTION ID>
```

We can get the required API key and IDs from our Appwrite console as shown below:

![API key](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691711034772_Screenshot+2023-08-04+at+01.45.30.png)

![Project ID](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691711069809_Screenshot+2023-08-04+at+01.24.17.png)

![Database and Collection ID](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691711142955_Screenshot+2023-08-11+at+00.44.58.png)

**Create the API models**

Next, we need to create models to represent our application data. To do this, we need to modify the `models.rs` file as shown below:

```rust
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Project {
    #[serde(rename = "$id")]
    pub id: Option<String>,
    pub name: String,
    pub description: String,
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ProjectRequest {
    pub name: String,
    pub description: String,
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ProjectResponse {
    #[serde(rename = "$id")]
    pub id: String,
    #[serde(rename = "$collectionId")]
    pub collection_id: String,
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct JsonAPIBody {
    pub documentId: Option<String>,
    pub data: ProjectRequest,
}

#[derive(Serialize, Debug, Clone)]
pub struct APIResponse<T> {
    pub status: u16,
    pub message: String,
    pub data: Option<T>,
}

#[derive(Serialize, Debug, Clone)]
pub struct APIErrorResponse {
    pub status: u16,
    pub message: String,
    pub data: Option<String>,
}
```

The snippet above does the following:

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

**_PS_**: _The_ `#[serde(rename = "FieldName")]` _macro renames the corresponding field to a specified name, and the `#[derive()]` macro adds implementation support for serialization, deserialization, debugging, and cloning._

**Create the API services**

With our application models fully set up, we can now use them to create our application logic. To do this, we need to update the `service.rs` file by doing the following:

First, we need to import the required dependencies, create helper functions, and a method for creating a project:

```rust
use dotenv::dotenv;
use reqwest::{header, Client, Error};
use std::env;
use super::model::{JsonAPIBody, Project, ProjectRequest, ProjectResponse};

pub struct AppwriteService {}

impl AppwriteService {
    fn env_loader(key: &str) -> String {
        dotenv().ok();
        match env::var(key) {
            Ok(v) => v.to_string(),
            Err(_) => format!("Error loading env variable"),
        }
    }

    fn init() -> Client {
        Client::new()
    }

    pub async fn create_project(new_project: ProjectRequest) -> Result<ProjectResponse, Error> {
        //get details from environment variable
        let project_id = AppwriteService::env_loader("PROJECT_ID");
        let database_id = AppwriteService::env_loader("DATABASE_ID");
        let collection_id = AppwriteService::env_loader("COLLECTION_ID");
        let api_key = AppwriteService::env_loader("API_KEY");

        let url = format!("https://cloud.appwrite.io/v1/databases/{database_id}/collections/{collection_id}/documents");

        //create header
        let mut headers = header::HeaderMap::new();
        headers.insert("X-Appwrite-Key", api_key.parse().unwrap());
        headers.insert("X-Appwrite-Project", project_id.parse().unwrap());

        let client = AppwriteService::init()
            .post(url)
            .headers(headers)
            .json(&JsonAPIBody {
                documentId: Some("unique()".to_string()),
                data: new_project,
            })
            .send()
            .await;

        match client {
            Ok(response) => {
                let json = response.text().await?;
                let created_project: ProjectResponse = serde_json::from_str(json.as_str()).unwrap();
                Ok(created_project)
            }
            Err(error) => Err(error),
        }
    }
}
```

The snippet above does the following:

-   Imports the required dependencies
-   Creates an `AppwriteService` struct
-   Creates an implementation block that adds `env_loader` and `init` helper methods to load environment variables and creates a connection pool for making asynchronous requests
-   Creates a `create_project` method that uses the helper methods to get the required environment variable, configure the Appwrite’s provisioned server URL, make a request, and return appropriate responses.

**_PS_**: _The `unique()` tag specified when creating a project tells Appwrite to autogenerate the project ID._

Secondly, we need to add a `get_project` method that uses similar logic as the `create_project` function to get the details of a project.

```rust
//imports goes here

pub struct AppwriteService {}

impl AppwriteService {
    //helper method goes here

    pub async fn create_project(new_project: ProjectRequest) -> Result<ProjectResponse, Error> {
        //create_project code goes here
    }

    pub async fn get_project(document_id: String) -> Result<Project, Error> {
        //get details from environment variable
        let project_id = AppwriteService::env_loader("PROJECT_ID");
        let database_id = AppwriteService::env_loader("DATABASE_ID");
        let collection_id = AppwriteService::env_loader("COLLECTION_ID");
        let api_key = AppwriteService::env_loader("API_KEY");

        let url = format!("https://cloud.appwrite.io/v1/databases/{database_id}/collections/{collection_id}/documents/{document_id}");

        //create header
        let mut headers = header::HeaderMap::new();
        headers.insert("X-Appwrite-Key", api_key.parse().unwrap());
        headers.insert("X-Appwrite-Project", project_id.parse().unwrap());

        let client = AppwriteService::init()
            .get(url)
            .headers(headers)
            .send()
            .await;

        match client {
            Ok(response) => {
                let json = response.text().await?;
                let project_detail: Project = serde_json::from_str(json.as_str()).unwrap();
                Ok(project_detail)
            }
            Err(error) => Err(error),
        }
    }
}
```

Thirdly, we need to add a `update_project` method that uses similar logic as the `create_project` function to update the details of a project.

```rust
//imports go here

pub struct AppwriteService {}

impl AppwriteService {
    //helper method goes here

    pub async fn create_project(new_project: ProjectRequest) -> Result<ProjectResponse, Error> {
        //create_project code goes here
    }

    pub async fn get_project(document_id: String) -> Result<Project, Error> {
        //get_project goes here
    }

    pub async fn update_project(
        updated_project: ProjectRequest,
        document_id: String,
    ) -> Result<ProjectResponse, Error> {
        //get details from environment variable
        let project_id = AppwriteService::env_loader("PROJECT_ID");
        let database_id = AppwriteService::env_loader("DATABASE_ID");
        let collection_id = AppwriteService::env_loader("COLLECTION_ID");
        let api_key = AppwriteService::env_loader("API_KEY");

        let url = format!("https://cloud.appwrite.io/v1/databases/{database_id}/collections/{collection_id}/documents/{document_id}");

        //create header
        let mut headers = header::HeaderMap::new();
        headers.insert("X-Appwrite-Key", api_key.parse().unwrap());
        headers.insert("X-Appwrite-Project", project_id.parse().unwrap());

        let client = AppwriteService::init()
            .patch(url)
            .headers(headers)
            .json(&JsonAPIBody {
                documentId: None,
                data: updated_project,
            })
            .send()
            .await;

        match client {
            Ok(response) => {
                let json = response.text().await?;
                let updates: ProjectResponse = serde_json::from_str(json.as_str()).unwrap();
                Ok(updates)
            }
            Err(error) => Err(error),
        }
    }
}
```

Lastly, we need to add a `delete_project` method that uses similar logic as the `create_project` function to delete the details of a project.

```rust
//import goes here

pub struct AppwriteService {}

impl AppwriteService {
    //helper method goes here

    pub async fn create_project(new_project: ProjectRequest) -> Result<ProjectResponse, Error> {
        //create_project code goes here
    }

    pub async fn get_project(document_id: String) -> Result<Project, Error> {
        //get_project goes here
    }

    pub async fn update_project(
        updated_project: ProjectRequest,
        document_id: String,
    ) -> Result<ProjectResponse, Error> {
        //update_project code goes here
    }

    pub async fn delete_project(document_id: String) -> Result<String, Error> {
        //get details from environment variable
        let project_id = AppwriteService::env_loader("PROJECT_ID");
        let database_id = AppwriteService::env_loader("DATABASE_ID");
        let collection_id = AppwriteService::env_loader("COLLECTION_ID");
        let api_key = AppwriteService::env_loader("API_KEY");

        let url = format!("https://cloud.appwrite.io/v1/databases/{database_id}/collections/{collection_id}/documents/{document_id}");

        //create header
        let mut headers = header::HeaderMap::new();
        headers.insert("X-Appwrite-Key", api_key.parse().unwrap());
        headers.insert("X-Appwrite-Project", project_id.parse().unwrap());

        let client = AppwriteService::init()
            .delete(url)
            .headers(headers)
            .send()
            .await;

        match client {
            Ok(_) => {
                let json = format!("Project with ID: ${document_id} deleted successfully!!");
                Ok(json)
            }
            Err(error) => Err(error),
        }
    }
}
```

**Create the API handlers**

With that done, we can use the services to create our API handlers. To do this, first, we need to add the snippet below to the `handlers.rs` file:

```rust
use super::{
    model::{APIErrorResponse, APIResponse, Project, ProjectRequest, ProjectResponse},
    services::AppwriteService,
};
use actix_web::{
    delete, get, patch, post,
    web::{Json, Path},
    HttpResponse,
};
use reqwest::StatusCode;

#[post("/project")]
pub async fn create_project_handler(data: Json<ProjectRequest>) -> HttpResponse {
    let new_project = ProjectRequest {
        name: data.name.clone(),
        description: data.description.clone(),
    };
    let project_details = AppwriteService::create_project(new_project).await;

    match project_details {
        Ok(data) => HttpResponse::Accepted().json(APIResponse::<ProjectResponse> {
            status: StatusCode::CREATED.as_u16(),
            message: "success".to_string(),
            data: Some(data),
        }),
        Err(error) => HttpResponse::InternalServerError().json(APIErrorResponse {
            status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
            message: "failure".to_string(),
            data: Some(error.to_string()),
        }),
    }
}

#[get("/project/{id}")]
pub async fn get_project_handler(path: Path<String>) -> HttpResponse {
    let id = path.into_inner();
    if id.is_empty() {
        return HttpResponse::BadRequest().json(APIErrorResponse {
            status: StatusCode::BAD_REQUEST.as_u16(),
            message: "failure".to_string(),
            data: Some("invalid ID".to_string()),
        });
    };
    let project_details = AppwriteService::get_project(id).await;

    match project_details {
        Ok(data) => HttpResponse::Accepted().json(APIResponse::<Project> {
            status: StatusCode::OK.as_u16(),
            message: "success".to_string(),
            data: Some(data),
        }),
        Err(error) => HttpResponse::InternalServerError().json(APIErrorResponse {
            status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
            message: "failure".to_string(),
            data: Some(error.to_string()),
        }),
    }
}
```

The snippet above does the following:

-   Imports the required dependencies
-   Creates a `create_project_handler` and `get_project_handler` handler with corresponding API routes that use the services to perform the corresponding actions and return the appropriate response using the `APIResponse` and `APIErrorResponse`

Lastly, we need to add `update_project_handler` and `delete_project_handler` handler that uses similar logic as the handlers above to update and delete a project.

```rust
//import goes here

#[post("/project")]
pub async fn create_project_handler(data: Json<ProjectRequest>) -> HttpResponse {
    //create_project_handler code goes here
}

#[get("/project/{id}")]
pub async fn get_project_handler(path: Path<String>) -> HttpResponse {
    //get_project_handler code goes here
}

#[patch("/project/{id}")]
pub async fn update_project_handler(
    updated_project: Json<ProjectRequest>,
    path: Path<String>,
) -> HttpResponse {
    let id = path.into_inner();
    if id.is_empty() {
        return HttpResponse::BadRequest().json(APIErrorResponse {
            status: StatusCode::BAD_REQUEST.as_u16(),
            message: "failure".to_string(),
            data: Some("invalid ID".to_string()),
        });
    };
    let data = ProjectRequest {
        name: updated_project.name.clone(),
        description: updated_project.description.clone(),
    };
    let project_details = AppwriteService::update_project(data, id).await;

    match project_details {
        Ok(data) => HttpResponse::Accepted().json(APIResponse::<ProjectResponse> {
            status: StatusCode::OK.as_u16(),
            message: "success".to_string(),
            data: Some(data),
        }),
        Err(error) => HttpResponse::InternalServerError().json(APIErrorResponse {
            status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
            message: "failure".to_string(),
            data: Some(error.to_string()),
        }),
    }
}

#[delete("/project/{id}")]
pub async fn delete_project_handler(path: Path<String>) -> HttpResponse {
    let id = path.into_inner();
    if id.is_empty() {
        return HttpResponse::BadRequest().json(APIErrorResponse {
            status: StatusCode::BAD_REQUEST.as_u16(),
            message: "failure".to_string(),
            data: Some("invalid ID".to_string()),
        });
    };
    let project_details = AppwriteService::delete_project(id).await;

    match project_details {
        Ok(data) => HttpResponse::Accepted().json(APIResponse::<String> {
            status: StatusCode::ACCEPTED.as_u16(),
            message: "success".to_string(),
            data: Some(data),
        }),
        Err(error) => HttpResponse::InternalServerError().json(APIErrorResponse {
            status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
            message: "failure".to_string(),
            data: Some(error.to_string()),
        }),
    }
}
```

**Putting it all together**

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

```rust
use actix_web::{App, HttpServer};
use api::handlers::{
    create_project_handler, delete_project_handler, get_project_handler, update_project_handler,
};
mod api;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(move || {
        App::new()
            .service(create_project_handler)
            .service(get_project_handler)
            .service(update_project_handler)
            .service(delete_project_handler)
    })
    .bind(("localhost", 8080))?
    .run()
    .await
}
```

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:

```bash
cargo run main
```

![create](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691715978196_Screenshot+2023-08-05+at+17.11.09.png)
![update](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691715978260_Screenshot+2023-08-06+at+00.16.12.png)

![Get details](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691715978225_Screenshot+2023-08-05+at+22.31.35.png)
![Delete](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691715978285_Screenshot+2023-08-06+at+00.30.24.png)

We can also confirm the project management data by checking the collection on Appwrite.

![Detail](https://paper-attachments.dropboxusercontent.com/s_2095423646DCD7D2DA1B88D8D859DA299FF57589D0A10336F58D285B02BBBE7E_1691716190575_Screenshot+2023-08-11+at+02.08.04.png)

# Conclusion

This post discussed what Appwrite is and provided a detailed step-by-step guide to use it to build a project management API in Rust.

These resources may also be helpful:

-   [Appwrite official documentation](https://appwrite.io/docs?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog)
-   [Appwrite database API](https://appwrite.io/docs/client/databases?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog)
-   [Actix web framework](https://actix.rs/?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog)
-   [Time-saving with Appwrite](https://dev.to/hackmamba/save-time-earn-more-the-benefits-of-using-appwrite-cloud-for-freelance-developers-1m89)