Serverless Function is a single-purpose programming feature that allows developers to write and deploy software without purchasing and maintaining servers. It lets developers focus on the core business logic of their application without worrying about logistics, operation, and infrastructure associated with software development.
In this post, we will learn how to use Serverless Functions to build a user management API in Rust using Netlify, Xata, and Cloudinary.
Technology overview
Netlify is a platform for deploying and building highly-performant and dynamic websites, e-commerce stores, and web applications.
Xata is an HTTP API-based serverless database for building scalable applications. It supports a built-in search engine, branching, scalability, and durability without manually managing database configuration and deployment.
Cloudinary is a visual media service for uploading, storing, managing, transforming, and delivering images and videos for websites and applications.
GitHub links
The project source codes are below:
Prerequisites
To follow along with this tutorial, the following requirements apply:
- Basic understanding of Rust
- Basic understanding of JavaScript
- Node.js installed
- A Netlify account for deploying Serverless Functions. Signup is completely free
- A Xata account for storing data. Signup is completely free
- A Cloudinary account for image optimization. Signup is completely free.
- A Github account for saving source code. Signup is completely free. ## Set up a Database on Xata
To create a database for storing our user management API’s data, we need to log into our Xata’s workspace, click the Add a Database button, input users
as the database name, and Create.
Next, we need to create a table in our database. To do this, click the Start from scratch menu, input userDetails
as the table name, and Add table.
PS: Xata auto-generates an ID column (a unique identifier) for our table.
With that done, we need to add a firstName
, lastName
, phoneNumber
, and avatar
columns to our table. To do this, click on the Plus icon, select String, input column name, and Create column.
After creating the columns, our updated table should be similar to the screenshot below:
Get Database URL and set up API Key
By default, Xata provides a unique and secure URL for accessing the database. To get our database URL, click the Get code snippet button, copy the URL, and then click the Set up API key button to generate API key.
PS: The URL we need to copy starts from the *https://……*
section
Click the Add a key button, input xata-function
as the name, and Save.
We must copy and keep the URL and generated API key, as they will come in handy when building our serverless functions.
Image sourcing and upload to Cloudinary
Next, we need to upload an image we will use as a default avatar when creating a user.
Avatar url
- bit.ly/3gUBL7E
In our Cloudinary dashboard, we uploaded the image by clicking on the Media Library tab, clicking on Upload, selecting the Web Address option, inputting the URL, and clicking on the Arrow Button to upload.
After uploading the image, we will see it displayed on the console. To get the image URL, mouse hover on the image and click on the Copy URL icon. The URL will come in handy when building our Serverless Functions.
Creating Serverless Functions
To get started, we need to navigate to the desired directory and run the command below:
mkdir xata-functions && cd xata-functions
The command creates a directory called xata-functions
and navigates into it.
Initializing project and installing dependencies
First, we need to initialize an empty Node.js project by running the command below:
npm init -y
Lastly, we need to install node-fetch
, a package for making HTTP requests. To do this, we need to run the command below:
npm i node-fetch
Adding logics to the Serverless Functions
With that done, we can start creating our application logic. To get started; first, we need to create a netlify
folder and create a functions
folder in this folder.
Secondly, we need to create a create.js
file inside the functions
folder and add the snippet below:
import fetch from 'node-fetch';
exports.handler = async function (event, context, callback) {
let bodyRequest = JSON.parse(event.body);
const body = {
firstName: bodyRequest.firstName,
lastName: bodyRequest.lastName,
phoneNumber: bodyRequest.phoneNumber,
avatar: 'https://res.cloudinary.com/dtgbzmpca/image/upload/v1667083687/abstract-user-flat-4.png',
};
const response = await fetch(
`${process.env.XATA_URL}:main/tables/userDetails/data`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.XATA_API_KEY}`,
},
body: JSON.stringify(body),
}
);
const data = await response.json();
return {
statusCode: 200,
body: JSON.stringify(data),
};
};
The snippet above does the following:
- Imports the required dependency
- Gets the request body
- Uses the request body to create a
body
object by passing in thefirstName
,lastName
,phoneNumber
, andavatar
default URL we got from Cloudinary - Creates a
POST
request to the Xata database by passing the URL as an environment variable with the database details and API key - Returns the appropriate response.
We also constructed the Xata database URL by passing in the branch, table name, and endpoint type.
https://sample-databaseurl/users:<BRANCH NAME>/tables/<TABLE NAME>/ENDPOINT TYPE
We can get the required details from our workspace
In our case, we filled it using an environment variable. We will add it when deploying our application to Netlify. An adequately filled URL is below:
https://sample-databaseurl/users:main/tables/userDetails/data
Thirdly, we need to create a get.js
file inside the same functions
folder and add the snippet below:
import fetch from 'node-fetch';
exports.handler = async function () {
const body = {
page: {
size: 15,
},
};
const response = await fetch(
`${process.env.XATA_URL}:main/tables/userDetails/query`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.XATA_API_KEY}`,
},
body: JSON.stringify(body),
}
);
const data = await response.json();
return {
statusCode: 200,
body: JSON.stringify(data),
};
};
The snippet above works similarly to the create.js
file. However, we created a body
object to paginate the requested data from the Xata database.
Lastly, we need to add a deployment file that instructs Netlify to build our application effectively. To do this, we need to create a netlify.toml
file in the root directory of our project and add the snippet below:
[functions]
node_bundler = "esbuild"
Pushing our source code to GitHub
We need to push our source code to GitHub to enable a seamless deployment process. To get started, we need to log into our GitHub account, create a repository, input xata-functions
as the name and Create repository.
Next, we initialize a git
project and save recent changes in our project by running the command below:
git init
git add .
git commit -m "add serverless functions"
To push our changes, copy and run the highlighted commands on the terminal:
Deploying to Netlify
To get started, we need to log into our Netlify dashboard. Click on Add new site dropdown and select Import an existing project.
Select GitHub as the Git provider and authorize Netlify.
Search for xata-function
and select the repository.
Click on Show advanced, click the New variable button and add the XATA_URL
and XATA_API_KEY
environment variables as key and their corresponding values.
As earlier mentioned, we can get Xata’s URL and API key from our workspace.
Click on Deploy site button to start deployment. It might take a few minutes.
We can view and get our deployed Serverless Functions URL by navigating to the Functions tab, and click on any of the functions to access the URL.
Leveraging the Serverless Functions to build Rust APIs
With our Serverless Functions up and running, we can start leveraging them to build our user management APIs. To get started, we need to navigate to the desired directory and run the command below in our terminal:
cargo new rust-user-service && cd rust-user-service
This command creates a Rust project called rust-user-service
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:
//other code section goes here
[dependencies]
actix-web = "4"
serde = { version = "1.0.145", features = ["derive"] }
serde_json = "1.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. E.g. convert Rust structs to JSON and vice versa.
serde_json = "1.0"
is a crate that uses the serde
crate 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:
cargo build
Structuring our application
It is essential to have a good project structure as it makes the project maintainable and easier for us and others to read our codebase.
To do this, we need to navigate to the src
directory and, in this folder, create api
folder. Inside the api
folder, we also need to create mod.rs
, models.rs
, services.rs
, and handlers.rs
files.
mod.rs
is file for managing application visibility.
models.rs
is for structuring our application data.
services.rs
is for abstracting our application logics.
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:
mod api;
fn main() {
println!("Hello world")
}
Create the API models
Next, we need to create models to represent our application data. To do this, we need to modify the model.rs
file as shown below:
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct User {
pub id: Option<String>,
pub first_name: String,
pub last_name: String,
pub phone_number: String,
pub avatar: Option<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Records {
pub records: Vec<User>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CreateResponse {
pub id: String,
}
#[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
User
andRecords
structs with required properties to describe request and response body accordingly - Creates a
CreateResponse
,APIResponse
, andAPIErrorResponse
structs with the required properties needed for the API response
PS: The *#[serde(rename_all = "camelCase")]*
macro converts snake case properties to camel case and the *derive*
macro adds implementation support for serialization, deserializations, and cloning.
Create the API services
With our Serverless Functions up and running, we can use it to create a user service. To do this, we need to add the snippet below to the services.rs
file:
use super::models::{CreateResponse, Records, User};
use reqwest::{Client, Error};
pub struct UserService {}
impl UserService {
pub async fn get_user() -> Result<Records, Error> {
let url = "<NETLIFY FUNCTION GET URL>";
let client = Client::new();
let fetched_users = client.get(url).send().await;
match fetched_users {
Ok(response) => {
let json = response.text().await?;
let new_records: Records = serde_json::from_str(json.as_str()).unwrap();
Ok(new_records)
}
Err(error) => Err(error),
}
}
pub async fn create_user(new_user: User) -> Result<CreateResponse, Error> {
let url = "<NETLIFY FUNCTION CREATE URL>";
let json_body = User {
id: None,
first_name: new_user.first_name,
last_name: new_user.last_name,
phone_number: new_user.phone_number,
avatar: None,
};
let client = Client::new();
let fetched_users = client.post(url).json(&json_body).send().await;
match fetched_users {
Ok(response) => {
let json = response.text().await?;
let created_record: CreateResponse = serde_json::from_str(json.as_str()).unwrap();
Ok(created_record)
}
Err(error) => Err(error),
}
}
}
The snippet above does the following:
- Imports the required dependencies
- Creates a
UserService
struct - Creates an implementation block that adds methods to the
UserService
struct - Adds a
get_user
method to get the list of users by making an HTTP request to theget
Serverless Function URL and returns the list of users - Adds a
create_user
method to create a user by making an HTTP request to thecreate
Serverless Function URL and returns the appropriate response
As earlier mentioned, we can get our URLs from the Netlify function tab.
Create the API handlers
With that done, we can use the services to create our API handlers. To do this, we need to add the snippet below to handlers.rs
file:
use actix_web::{get, post, web::Json, HttpResponse};
use reqwest::StatusCode;
use super::{
models::{APIErrorResponse, APIResponse, CreateResponse, Records, User},
services::UserService,
};
#[get("/users")]
pub async fn get_user() -> HttpResponse {
let user_details = UserService::get_user().await;
match user_details {
Ok(data) => HttpResponse::Ok().json(APIResponse::<Records> {
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()),
}),
}
}
#[post("/users")]
pub async fn create_user(data: Json<User>) -> HttpResponse {
let new_user = User {
id: None,
first_name: data.first_name.clone(),
last_name: data.last_name.clone(),
phone_number: data.phone_number.clone(),
avatar: None,
};
let user_details = UserService::create_user(new_user).await;
match user_details {
Ok(data) => HttpResponse::Accepted().json(APIResponse::<CreateResponse> {
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()),
}),
}
}
The snippet above does the following:
- Imports the required dependencies
- Creates a
get_user
handler with a/users
API route that uses theget_user
service to get the list of users and returns the appropriate response using theAPIResponse
andAPIErrorResponse
- Creates a
create_user
handler with a/users
API route that uses thecreate_user
service to create a user and returns the appropriate response using theAPIResponse
andAPIErrorResponse
Putting it all together
With that done, we need to update the main.rs
file to include our application entry point and use the get_user
and create_user
handlers.
use actix_web::{App, HttpServer};
use api::handlers::{create_user, get_user};
mod api;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(move || App::new().service(get_user).service(create_user))
.bind(("localhost", 8080))?
.run()
.await
}
The snippet above does the following:
- Imports the required dependencies
- Creates a new server that adds the
get_user
andcreate_user
handlers and runs onlocalhost:8080
With that done, we can start a development server using the command below:
cargo run main.go
We can also verify the APIs by checking the Xata’s workspace
Conclusion
This post discussed how to quickly create user management APIs in Rust without manually deploying and managing databases and servers. With the powerful trio of Xata, Netlify, and Cloudinary, developers can build and ship applications faster by focusing on what matters and not being bothered by infrastructure bottlenecks.
These resources might be helpful: