Create a Serverless REST API using Cloudflare Workers, Rust & MongoDB

Rishabh
Intelliconnect Engineering
5 min readOct 30, 2023

In this article, we will create a serverless API using Cloudflare Workers and the Rust Programming Language. You can find the code for this article on Github.

We will connect to MongoDB using the MongoDB ‘Data API.’ Typically, our REST APIs connect to databases via TCP connections. However, Cloudflare Serverless APIs cannot utilize direct TCP connections to databases. Recently, Cloudflare introduced Cloudflare D1, a serverless SQL Database. For more information, please refer to the Cloudflare documentation titled Cloudflare D1.

Another option is to use Supabase — The Supabase community provides a crate for querying the database. You can check the connectivity tools provided by them here.

We are going to use the Native Rust SDK officially provided by Cloudflare to create an API.

Technologies Used:

  • Rust (Programming language)
  • Cloudflare Workers (Serverless Platform)
  • MongoDB (Database)

Setup:

  1. Install Wrangler, a CLI tool to build, test, and deploy workers. We are using Wrangler for this article, after installing, login to your account with it.
# for npm users  
npm install wrangler --save-dev

# for yarn users
yarn add --dev wrangler

# check if it's installed correctly
wrangler --version

# login to your account
wrangler login

2. Clone the workers template from Github.

# rust starter template for cloudflare workers 
git clone https://github.com/cloudflare/rustwasm-worker-template

# or clone this article code
git clone git@github.com:intelliconnect/cloudflare-serverless-rust.git

cd cloudflare_serverless-rust

3. Log in to your MongoDB account and go to Data services. Then, navigate to Data API, select users to create an API Key, and copy the given API key.

4. Store your MongoDB data API secret key, or any other secret key if you are using a different database, by following the command below. Create a secret named ‘mongo_data_api_key’ using the command wrangler secret put mongo_data_api_key. It will prompt you to enter the secret key, which you should paste and press 'Enter'. Then, create a file named .dev.vars in your root directory and paste the secret value and key in the below format:

# create a secret with name mongo_data_api_key 
wrangler secret put mongo_data_api_key
# it will ask for secret key, paste it and press enter
# create a filename as ‘.dev.vars’ in your root directory 
# paste your secret value and key here like below
mongo_data_api_key="your api key here"

Note: Remember that anyone who has access to the secret key can access your database.

4. First, we will write a helper function to make HTTP requests to MongoDB, and then create GET and POST routes for retrieving and storing a document in a collection.

Helper Function

In the src/utils.rs add the code below.

// mongo_request that takes input parameters 

pub async fn mongo_request(
method: worker::Method,
ctx: &RouteContext<()>,
body: &str,
query_type: &str,
query: &str,
) -> Result<String> {
// Retrieve the "mongo_data_api_key" secret from the context and convert it to a string
let api_key = ctx.secret("mongo_data_api_key")?.to_string();

// Build the URL for the MongoDB API request
let url = format!(
"https://ap-south-1.aws.data.mongodb-api.com/app/data-lqfly/endpoint/data/v1/{}/{}",
query_type, query
);

// Create a new set of headers and set the required headers
let mut headers = worker::Headers::new();
headers.set("Access-Control-Request-Headers", "*")?;
headers.set("Content-Type", "application/json")?;
headers.set("api-key", api_key.as_str())?;

// Create a new request initialization with the specified method, headers, and body
let mut request = RequestInit::new();
request
.with_method(method)
.with_headers(headers)
.with_body(Some(JsValue::from_str(body)));

let mut response = Fetch::Request(Request::new_with_init(&url, &request)?)
.send()
.await?;

response.text().await
}

You can take a look at MongoDB Data API Documents to improve this function or customize it in your own way. You can also take a look at worker-rs docs. Check the code for file utils.rs here.

GET a Document from MongoDB:

In the src/lib.rs file, we have got a router (similar to Express JS if you have used it before). It provides us with the request (contains request body, method, etc.) and context (contains the secrets, durable object, etc.). You can take a look at worker-rs docs for more information.

Create an endpoint /data as in the code below.

// get data from mongodb         
.get_async("/data", |_, ctx| async move {

let body = json!({
"dataSource": "Cluster0",
"database": "practice",
"collection": "rust-cf",
});
let body_str = body.to_string();
let res = match utils::mongo_request(
Method::Post,
&ctx, body_str.as_str(),
"action",
"find")
.await {
Ok(res) => res,
Err(err) => {
// Handle the error here.
return Response::error(err.to_string(), 500);
}
};
let res_json = serde_json::from_str::<serde_json::Value>(res.as_str()).unwrap();
Response::from_json(&res_json)
})

There are two types of method functions on the router struct, the async ones and without async ones. We are using async functions since we are fetching data from the database asynchronously. This function takes the endpoint name as the first argument and closure as the second argument. If you are using an async function, pass in a closure with the async move keyword. The closure takes two arguments: req (request) and ctx (context). We don’t require the request body in GET and use context to get the secret for MongoDB.

POST a document to MongoDB:

In the src/lib.rs file, add the code below.

//post data to mongodb 
.post_async("/data", |mut req, ctx| async move {
let json_data = req.json::<serde_json::Value>().await?;
let body = json!({
"dataSource": "Cluster0",
"database": "practice",
"collection": "rust-cf",
"document": json_data
});
let body_str = body.to_string();
let res = match utils::mongo_request(Method::Post,
&ctx, body_str.as_str(),
"action",
"insertOne")
.await {
Ok(res) => res,
Err(err) => {
// Handle the error here.
return Response::error(err.to_string(), 500);
}
};
let res_json = serde_json::from_str::<serde_json::Value>(res.as_str()).unwrap();
Response::from_json(&res_json)
})

We are using the req argument here to get the JSON body sent to this endpoint. Other than that, there is not much difference here from the get endpoint. Here we are doing the insertOne operation on the MongoDB collection.

Running the API:

From the root of the project, you can run the following command to test locally.

wrangler dev 

This will start a server locally on port 8787 by default. If you are using Wrangler 1, then it sends the request to your Cloudflare worker online, but if you are using Wrangler 2, then it runs a local worker (it uses miniflare).

For publishing your worker online, you can run the below command.

wrangler publish

For more options on publishing the worker and other configuration, you can take a look at Wrangler docs.

Testing:

For testing, if you want to use the command line, I suggest httpie. For testing GET requests, you can run the following command.

# using httpie or postman 
http localhost:8787/data

# using curl
curl localhost:8787/data

For a JSON POST request.

http POST localhost:8787/data description=testing\ cloudflare\ worker flag:=true

Now, the above is a POST request with a JSON payload. You can refer to the httpie documentation for further information here.

If you don’t want to use a CLI tool and want to keep a record of APIs, you can use the usual suspect Postman.

Conclusion:

You can find the complete code here in this Github repository. I have also added two additional endpoints to get and set key-value pairs on Cloudflare KV store in the code. If you have any queries, please feel free to reach out.

Hope this helps you to start your journey in Serverless Programming using Cloudflare Workers. Thank you for reading.

Credits:

My sincere thanks to my colleague Mustafa Ansari at Intelliconnect for helping me detail the article. Please note that with continuous releases by the cloudflare team, code may need additional tweaks. Please refer documentation from succeeding releases.

--

--