Introduction
Rust is a statically and strongly typed programming language that focuses on performance and safety. GraphQL is a query language for APIs that provides more flexibility to clients as compared to traditional REST APIs. This means, clients can request the data they need without making multiple requests to the API.
Below are benefits of using Rust for GraphQL APIs:
- Performance: Rust is a compiled language that aims to offer high performance. It can match C and C++ language speed with some additional features like memory and concurrency safety
- Memory management: Rust manages memory at compile time rather than relying on a garbage collection system
- Type safety: Rust's type system ensures that the data in a GraphQL API is correct. It's able to catch type errors at compile time before they reach production
- Concurrency: Rust allows building concurrent applications using multiple cores to handle multiple requests simultaneously
This article explains how to build a GraphQL API with Rust that stores data in a Rcs Managed PostgreSQL Database. You are to build a GraphQL server in Rust, then, push a Docker image to the Docker Hub Image Registry, and deploy the GraphQL server image to a Rcs Cloud Instance.
Prerequisites
Before you start:
- Deploy a OneClick Docker Server using the Rcs marketplace application
- Deploy a Rcs Managed Database for PostgreSQL
On your local development machine:
Install the Rust toolchain, including cargo (Rust version >= 1.65)
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shWhen installed, activate it using the following command
$ source "$HOME/.cargo/env"Verify the install Rust and Cargo versions
$ rustc --version && cargo --versionInstall Docker
Build a GraphQL Server in Rust
The GraphQL Server in this article is a Rust backend for an application called rustpaste built with async-graphql and axum libraries. The application allows users to store and share plain text online. Build the server and prepare it for production deployment as described in the steps below.
Create the Project
Using
cargo, create a new Rust project namedrustpaste$ cargo new rustpasteThe above command creates a new
Cargopackage, including aCargo.tomland asrc/main.rsfile in therustpastedirectory.Switch to the new directory
$ cd rustpasteVerify that the directory contains new files
$ lsOutput:
Cargo.toml srcBack up the
Cargo.tmlfile$ cp Cargo.toml Cargo.ORIGUsing a text editor such as
Nano, edit theCargo.tomlfile$ nano Cargo.tomlUpdate the existing content with the following configurations
[package] name = "rustpaste" version = "0.1.0" edition = "2021" [dependencies] async-graphql = "5.0.10" async-graphql-axum = "5.0.10" axum = "0.6.18" dotenv = "0.15.0" nanoid = "0.4.0" serde = { version = "1.0.164", features = ["derive"] } sqlx = { version = "0.7.1", features = ["runtime-tokio-native-tls", "json", "postgres"] } thiserror = "1.0.44" tokio = { version = "1.28.2", features = ["full"] }Save and close the file
Set Up the Database
Using
cargo, install thesqlx-clipackage$ cargo install sqlx-cliIf the above command returns an error, verify that the OpenSSL package is available on your system. If not install it using the command below:
$ sudo apt install libssl-devCreate a new
.envfile$ nano .envAdd the following string to the file. Replace
DATABASE_URLwith your Rcs Managed Database for PostgreSQL connection stringDATABASE_URL=postgresql://user:password@locahost:host/databaseSave and close the file
Using
sqlx-cli, create a new database$ sqlx database createAdd the first migration to create a new SQL table
$ sqlx migrate add add_paste_tableEdit the newly added
.sqlfile in themigrationsfolder$ nano migratons/.sqlAdd the following code to the file
CREATE TABLE paste ( id serial PRIMARY KEY, title text NOT NULL, content text NOT NULL, password text );Save and close the file
Start migration to create a new
pastedatabase table using your SQL file and.envconnection string$ sqlx migrate run
Create a new Rust Storage Layer
Switch to the
srcdirectory$ cd srcCreate a new file named
lib.rs$ nano src/lib.rsAdd the following code to the file
use async_graphql::futures_util::lock::Mutex; use std::sync::Arc; use thiserror::Error; mod storage; pub use storage::PasteStorage; #[derive(Clone)] pub struct Paste { id: String, title: String, content: String, password: Option<String>, } #[derive(Debug, Error)] pub enum PasteError { #[error("invalid id")] InvalidId, #[error("invalid password")] InvalidPassword, #[error("database error: {0}")] DatabaseError(#[from] sqlx::Error), }Save and close the file
The above code creates a new struct named
Pasteand a new enumPasteError. The StructPastecontains data of each paste, and the enumPasteErrorimplements the traitErrorfromthiserrorto simplify error handling.Create another file named
storage.rs$ nano src/storage.rsAdd the following code to the file
use sqlx::{Pool, Postgres}; use crate::{Paste, PasteError}; pub struct PasteStorage { pool: Pool<Postgres>, } impl PasteStorage { pub fn new(pool: Pool<Postgres>) -> Self { PasteStorage { pool } } } impl PasteStorage { pub async fn insert( &mut self, id: String, title: String, content: String, password: Option<String>, ) -> Result<Paste, PasteError> { let paste = Paste { id, title, content, password, }; sqlx::query!( "INSERT INTO paste VALUES ($1, $2, $3, $4)", paste.title, paste.content, paste.password ) .execute(&self.pool) .await?; Ok(paste) } pub async fn update( &mut self, id: String, title: String, content: String, password: Option<String>, ) -> Result<Paste, PasteError> { let paste = Paste { id, title, content, password, }; sqlx::query!( "UPDATE paste set title=$2, content=$3, password=$4 where id=$1", paste.id, paste.title, paste.content, paste.password ) .execute(&self.pool) .await?; Ok(paste) } pub async fn remove(&mut self, id: &str) -> Result<(), PasteError> { sqlx::query!("DELETE FROM paste WHERE id=$1", id) .execute(&self.pool) .await?; Ok(()) } pub async fn get(&self, id: &str) -> Result<Option<Paste>, PasteError> { let result = sqlx::query!( "SELECT id, title, content, password FROM paste WHERE id=$1", id ) .fetch_optional(&self.pool) .await?; match result { Some(row) => Ok(Some(Paste { id: row.id, title: row.title, content: row.content, password: row.password, })), None => Ok(None), } } pub async fn get_all(&self) -> Result<Vec<Paste>, PasteError> { let results = sqlx::query!("SELECT id, title, content, password FROM paste") .fetch_all(&self.pool) .await?; let mut pastes = vec![]; for row in results { let p = Paste { id: row.id, title: row.title, content: row.content, password: row.password, }; pastes.push(p); } Ok(pastes) } }
<<<<<<< HEAD Save and close the file
Save and close the file.> 07acdedf1d73fde7de27c46263872b4c35f0eae8
The above code creates a new struct named `PasteStorage` that contains a pool to the PostgreSQL database. Later declarations contain methods to insert, update and query the data.Create a Storage Layer
Edit the
main.rsfile$ nano main.rsAdd the following contents to the file
use dotenv::dotenv; use nanoid::nanoid; use rustpaste::PasteStorage; use sqlx::PgPool; use std::env; async fn example(storage: &mut PasteStorage) { let id = nanoid!(); let result = storage .insert(id, "new title".to_string(), "new content".to_string(), None) .await; match result { Err(e) => println!("{}", e), Ok(p) => println!("Inserted {:?}", p), } let results = storage.get_all().await; match results { Err(_) => println!("Something went wrong!"), Ok(pastes) => { for p in pastes { println!("{:?}", p); } } } } #[tokio::main] async fn main() { dotenv().ok(); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); let pool = PgPool::connect(&database_url).await.unwrap(); let mut storage = PasteStorage::new(pool); example(&mut storage).await; }
<<<<<<< HEAD Save and close the file
Save and close the file.> 07acdedf1d73fde7de27c46263872b4c35f0eae8
Below is what the `main` method does:
* Parses the `.env` file for the database connection and creates a pool object for the PostgreSQL database.
* Create a mutable variable `storage` using the `PasteStorage::new` method
* Calls the `example` method with a mutable reference to the `storage` variable
* The `example` method creates a paste with a random id using the `nanoid` crate, inserts the paste into the database, and queries all the existing pastesRun the program
$ cargo runYour output should look like the one below:
Finished dev [unoptimized + debuginfo] target(s) in 0.04s Running `target/debug/rustpaste` Inserted Paste { id: "68O__4C5uzhXS37ICW4df", title: "new title", content: "new content", password: None } Paste { id: "68O__4C5uzhXS37ICW4df", title: "new title", content: "new content", password: None }
Set Up the GraphQL Server
Edit
lib.rsfile$ nano lib.rsReplace the existing code with the following contents
use async_graphql::futures_util::lock::Mutex; use async_graphql::{Context, EmptySubscription, Object, Schema}; use nanoid::nanoid; use std::str; use std::sync::Arc; use thiserror::Error; pub type ServiceSchema = Schema<QueryRoot, MutationRoot, EmptySubscription>; pub struct QueryRoot; pub struct MutationRoot; mod storage; pub use storage::PasteStorage; pub type Storage = Arc<Mutex<PasteStorage>>; #[derive(Clone, Debug)] pub struct Paste { id: String, title: String, content: String, password: Option<String>, } #[derive(Debug, Error)] pub enum PasteError { #[error("invalid id")] InvalidId, #[error("invalid password")] InvalidPassword, #[error("database error: {0}")] DatabaseError(#[from] sqlx::Error), } #[Object] impl Paste { async fn id(&self) -> &str { &self.id } async fn title(&self) -> &str { &self.title } async fn content(&self) -> &str { &self.content } } #[Object] impl QueryRoot { pub async fn hello(&self) -> &'static str { "Hello RustPaste" } pub async fn all_pastes(&self, ctx: &Context<'_>) -> Result<Vec<Paste>, PasteError> { let storage = ctx.data_unchecked::<Storage>().lock().await; storage.get_all().await } pub async fn paste(&self, ctx: &Context<'_>, id: String) -> Result<Option<Paste>, PasteError> { let storage = ctx.data_unchecked::<Storage>().lock().await; storage.get(&id).await } } #[Object] impl MutationRoot { async fn create_paste( &self, ctx: &Context<'_>, title: String, content: String, password: Option<String>, ) -> Result<Paste, PasteError> { let mut storage = ctx.data_unchecked::<Storage>().lock().await; let id = nanoid!(); storage.insert(id, title, content, password).await } async fn update_paste( &self, ctx: &Context<'_>, id: String, title: String, content: String, password: Option<String>, ) -> Result<Paste, PasteError> { let mut storage = ctx.data_unchecked::<Storage>().lock().await; let paste = storage.get(&id).await?; match paste { None => Err(PasteError::InvalidId), Some(paste) => match paste.password { None => storage.update(id, title, content, paste.password).await, Some(stored_pass) => match password { None => Err(PasteError::InvalidPassword), Some(input_pass) => { if stored_pass == input_pass { return storage.update(id, title, content, Some(input_pass)).await; } Err(PasteError::InvalidPassword) } }, }, } } async fn delete_paste( &self, ctx: &Context<'_>, id: String, password: Option<String>, ) -> Result<bool, PasteError> { let mut storage = ctx.data_unchecked::<Storage>().lock().await; let paste = storage.get(&id).await?; match paste { None => Err(PasteError::InvalidId), Some(paste) => match paste.password { None => Ok(false), Some(stored_pass) => match password { None => Err(PasteError::InvalidPassword), Some(input_pass) => { if stored_pass == input_pass { storage.remove(&id).await?; return Ok(true); } Err(PasteError::InvalidPassword) } }, }, } } }Save and close the file
Below is what the above configuration does:
- Creates a new struct
QueryRootand structMutationRoot - Implements methods for querying, inserting, updating, and deleting pastes
- Each method uses the
Contextfromasync_graphqlto access the sharedStorage
- Creates a new struct
Edit the
main.rsfile$ nano main.rsReplace the existing code with the following contents
use async_graphql::futures_util::lock::Mutex; use async_graphql::http::GraphiQLSource; use async_graphql::{EmptySubscription, Schema}; use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; use axum::response::{self, IntoResponse}; use axum::{routing::get, Extension, Router, Server}; use dotenv::dotenv; use sqlx::PgPool; use std::env; use std::sync::Arc; use rustpaste::{MutationRoot, PasteStorage, QueryRoot, ServiceSchema}; pub async fn graphql_handler( schema: Extension<ServiceSchema>, req: GraphQLRequest, ) -> GraphQLResponse { schema.execute(req.into_inner()).await.into() } pub async fn graphiql() -> impl IntoResponse { response::Html(GraphiQLSource::build().endpoint("/").finish()) } #[tokio::main] async fn main() { dotenv().ok(); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); let pool = PgPool::connect(&database_url).await.unwrap(); let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription) .data(Arc::new(Mutex::new(PasteStorage::new(pool)))) .finish(); let app = Router::new() .route("/", get(graphiql).post(graphql_handler)) .layer(Extension(schema)); println!("Server is running at http://0.0.0.0:8080"); Server::bind(&"0.0.0.0:8080".parse().unwrap()) .serve(app.into_make_service()) .await .unwrap(); }Save and close the file
The above code uses the
async_graphqlwithaxumto create a GraphQL server running on your localhost port8080.Run the application
$ cargo runUsing a web browser such as Firefox, access the
GraphQL IDEon port8080as belowhttp://localhost:8080/Run the query below
{ hello }Your output should look like the one below:
{ "data": { "hello": "Hello RustPaste" } }Run another query
{ allPastes { id title } }Output:
{ "data": { "allPastes": [ { "id": "68O__4C5uzhXS37ICW4df", "title": "new title" }, { "id": "cH9T9zDs3pTdqc39_9JqO", "title": "new title" } ] } }
Access the GraphQL API using Curl
In your terminal session, run the following command to query all pastes
$ curl 'http://localhost:8080/' \ -H 'content-type: application/json' \ --data '{ "query": "{ allPastes { id title} }" }'Query one paste
$ curl 'http://localhost:8080/' \ -H 'content-type: application/json' \ --data '{ "query": "{ paste (id: \"5o8efIH6NPUROuQa2DGu3\") { id title content} }" }'Create one paste
$ curl 'http://localhost:8080/' \ -H 'content-type: application/json' \ --data '{ "query": "mutation { createPaste (title: \"new title\", content: \"new content\") { id title content} }" }'Update a single paste
$ curl 'http://localhost:8080/' \ -H 'content-type: application/json' \ --data '{ "query": "mutation { updatePaste (id: \"wv6FqXCJitgxpJP53gd-X\", title: \"updated title\", content: \"updated content\") { id title content} }" }'Delete a single paste
$ curl 'http://localhost:8080/' \ -H 'content-type: application/json' \ --data '{ "query": "mutation { deletePaste (id: \"KUzUcOMss8J1s41J-riXp\") }" }'
Build and Push a Docker image to Docker Hub
Switch to the project directory
$ cd rustpasteCreate a new
Dockerfile$ nano DockerFileAdd the following contents to the file
FROM rust:bookworm as builder WORKDIR /usr/src/rustpaste COPY . . RUN cargo install --path . FROM debian:bookworm-slim RUN apt-get update && apt-get install -y libssl3 && rm -rf /var/lib/apt/lists/* COPY --from=builder /usr/local/cargo/bin/rustpaste /usr/local/bin/rustpaste CMD ["rustpaste"]Save and close the file.
The above configuration uses
rust:bookworm, which includes the Rust toolchain, to build the application. The runner stage uses thedebian:bookworm-slimDebian 12 Docker image with a small size, installs thelibssl3package and copies therustpastebinary from the builder stage.Build the Docker image
$ docker build . -t rustpasteVerify that the Docker image is available on your server
$ docker imagesOutput:
REPOSITORY TAG IMAGE ID CREATED SIZE rustpaste latest abe0709550ce 1 minutes ago 93.9MTo test that your Docker image works, deploy it on your development machine as below.
$ docker run --rm -p 8080:8080 -e DATABASE_URL='YOUR_DATABASE_URL' rustpasteReplace
YOUR_DATABASE_URLwith theDATABASE_URLvalue in your.envfileUsing
curl, verify that the application listens on the host port8080$ curl 127.0.0.1:8080Log in to Docker Hub
$ docker loginTag the Docker Image with your Docker Hub ID. Replace
example-userwith your actual Docker ID$ docker tag rustpaste example-user/rustpaste:latestPush the Docker Image to Docker Hub. Replace
example-userwith your actual Docker Hub account$ docker push example-user/rustpaste:latest
Deploy the GraphQL API Application on your Production Server
To deploy your GraphQL API application on your Rcs production server, verify that docker actively runs on the server, create the necessary local directories, and deploy the application as described in the following steps
Create a non-root sudo user
Switch to the sudo user account
# su example-user
Create a new
rustpastesubdirectory in the/vardirectory$ mkdir /var/rustpasteSwitch to the directory
$ cd /var/rustpasteCreate a new
.envfile$ nano .envAdd the following configuration to the file. Replace
postgresql://user:password@locahost:host/databasewith your actual Rcs Managed Database for PostgreSQL connection stringDATABASE_URL=postgresql://user:password@locahost:host/database
<<<<<<< HEAD
If you apply a new database different from your development database, copy the DATABASE_URL to your development machine and use sqlx-cli to run the database migration again
If you apply a new database different from your development database, copy the `DATABASE_URL` to your development machine and use `sqlx-cli` to run the database migration.> 07acdedf1d73fde7de27c46263872b4c35f0eae8
$ sqlx migrate runDeploy a Docker container with a restart
alwayspolicy. Replaceexample-userwith your actual Docker Hub account ID$ docker run -d --restart always --env-file .env -p 8080:8080 example-user/rustpaste:latestAllow the application port
8080through the UFW firewall to accept incoming connections$ sudo ufw allow 8080/tcpIn a new web browser window, load port
8080on your public Server IP to access the applicationhttp://<YOUR_SERVER_IP>:8080
Conclusion
In this article, you managed to build a GraphQL server with Rust, created a Docker image and deployed the application to a production Rcs Cloud Server. By using GraphQL, you can have a fast, reliable, and high-performance backend server. To access the full application source code, visit the RustPaste repository.
Next Steps
To secure your GraphQL API and implement more solutions on your Rcs Production server, visit the following resources: