In this tutorial, we will build a WebAssembly-powered serverless platform using Rust 🦀. This platform will enable you to register and invoke WebAssembly functions dynamically. We’ll also explore how to harness Rust’s performance, type safety, and concurrency features to build a scalable, robust system.
WebAssembly is an excellent choice for serverless platforms because of its portability, security model, and near-native performance. By compiling functions to WebAssembly, developers can write in any language that supports WASM compilation while the platform remains agnostic to the source language.
You can find the complete source code for this tutorial on GitHub:
https://github.com/luishsr/rust-serverless
Step 1: Setting Up the Project
First, create a new Rust project. We’ll structure the project as a binary crate for the main application and include libraries for modularity.
cargo new rust-serverless
cd rust-serverless
Update the Cargo.toml
file to include the necessary dependencies:
[dependencies]
warp = “0.3”
serde = { version = “1.0”, features = [“derive”] }
serde_json = “1.0”
sled = “0.34”
wasmtime = “27.0”
tokio = { version = “1.0”, features = [“full”] }
These dependencies enable us to build a web server (warp
), serialize and deserialize JSON (serde
), store registered functions (sled
), and execute WebAssembly modules (wasmtime
).
Step 2: Designing the Platform Architecture
The platform will have the following main components:
- HTTP Server: Built using
warp
, this will handle API requests for registering and invoking functions. - Storage Layer: Backed by
sled
, it will persist WebAssembly modules for registered functions. - Execution Engine: Using
wasmtime
, this will dynamically execute WebAssembly functions with supplied inputs.
We will organize these components into modules: api.rs
for the HTTP server, storage.rs
for the storage layer, and executor.rs
for the WebAssembly execution engine.
Step 3: Implementing the Storage Layer
The storage layer is responsible for persisting WebAssembly modules under unique names. We’ll use sled
, a fast and simple key-value store.
Create a file called src/storage.rs
:
use sled::Db;
pub struct Storage {
db: Db,
}
impl Storage {
pub fn init_with_path(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
let db = sled::open(path)?;
Ok(Self { db })
}
pub fn init() -> Result<Self, Box<dyn std::error::Error>> {
Self::init_with_path(“functions_db”)
}
pub fn save_function(&self, name: String, code: String) -> Result<(), sled::Error> {
self.db.insert(name, code.as_bytes())?;
Ok(())
}
pub fn load_function(&self, name: &str) -> Result<String, sled::Error> {
if let Some(code) = self.db.get(name)? {
Ok(String::from_utf8(code.to_vec()).unwrap())
} else {
Err(sled::Error::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
“Function not found”,
)))
}
}
}
This module provides methods to save and retrieve WebAssembly modules. It uses sled
to persist the mappings of function names to their Base64-encoded WASM binaries.
Step 4: Implementing the Execution Engine
The execution engine will use wasmtime
to dynamically load and execute WebAssembly modules. Create a file called src/executor.rs
:
use wasmtime::*;
use serde_json::Value;
pub fn execute(code: &str, function_name: &str, inputs: &[Value]) -> Result<Value, Box<dyn std::error::Error>> {
let engine = Engine::default();
let module = Module::new(&engine, code)?;
let mut store = Store::new(&engine, ());
let instance = Instance::new(&mut store, &module, &[])?;
let func = instance.get_func(&mut store, function_name)
.ok_or_else(|| format!(“Function ‘{}’ not found in module”, function_name))?;
let func_ty = func.ty(&store);
let params: Vec<_> = func_ty.params().collect();
let results: Vec<_> = func_ty.results().collect();
if params.len() != inputs.len() {
return Err(format!(
“Function ‘{}’ expected {} arguments, but got {}”,
function_name, params.len(), inputs.len()
).into());
}
let mut wasm_inputs = Vec::new();
for (param, input) in params.iter().zip(inputs.iter()) {
let value = match (param, input) {
(ValType::I32, Value::Number(n)) => Val::I32(n.as_i64().ok_or(“Invalid i32”)? as i32),
_ => return Err(format!(“Unsupported parameter type: {:?}”, param).into()),
};
wasm_inputs.push(value);
}
let mut wasm_results = vec![Val::I32(0); results.len()];
func.call(&mut store, &wasm_inputs, &mut wasm_results)?;
if wasm_results.len() > 1 {
return Err(“Multiple return values are not supported yet”.into());
}
let result = match wasm_results.get(0) {
Some(Val::I32(v)) => Value::Number((*v).into()),
_ => return Err(“Unsupported return type”.into()),
};
Ok(result)
}
This module dynamically loads a WebAssembly module, executes the specified function, and returns the result.

Step 5: Implementing the API Layer
The API layer will provide HTTP endpoints for registering and invoking functions. Create a file called src/api.rs
:
use warp::Filter;
use std::sync::Arc;
use crate::{executor, storage};
use serde_json::Value;
pub fn server(
storage: Arc<storage::Storage>,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
let register = warp::post()
.and(warp::path(“register”))
.and(warp::body::json())
.and(with_storage(storage.clone()))
.and_then(register_function);
let invoke = warp::post()
.and(warp::path(“invoke”))
.and(warp::body::json())
.and(with_storage(storage.clone()))
.and_then(invoke_function);
register.or(invoke)
}
fn with_storage(
storage: Arc<storage::Storage>,
) -> impl Filter<Extract = (Arc<storage::Storage>,), Error = std::convert::Infallible> + Clone {
warp::any().map(move || storage.clone())
}
async fn register_function(
body: Value,
storage: Arc<storage::Storage>,
) -> Result<impl warp::Reply, warp::Rejection> {
let function_name = body[“name”].as_str().ok_or_else(warp::reject::not_found)?;
let code = body[“code”].as_str().ok_or_else(warp::reject::not_found)?;
storage
.save_function(function_name.to_string(), code.to_string())
.map_err(|_| warp::reject::custom(warp::reject()))?;
Ok(warp::reply::json(&format!(“Function {} registered!”, function_name)))
}
async fn invoke_function(
body: Value,
storage: Arc<storage::Storage>,
) -> Result<impl warp::Reply, warp::Rejection> {
let function_name = body[“name”].as_str().ok_or_else(warp::reject::not_found)?;
let input = body[“input”].as_array().ok_or_else(warp::reject::not_found)?;
let code = storage.load_function(function_name).map_err(|_| warp::reject::not_found())?;
let result = executor::execute(&code, function_name, input).map_err(|_| warp::reject::not_found())?;
Ok(warp::reply::json(&result))
}
Step 6: Running the Platform
Finally, create src/main.rs
to start the server:
mod api;
mod executor;
mod storage;
use std::sync::Arc;
#[tokio::main]
async fn main() {
let storage = Arc::new(storage::Storage::init().expect(“Failed to initialize storage”));
warp::serve(api::server(storage))
.run(([127, 0, 0, 1], 3030))
.await;
Running it!
Start the server running:
cargo run
Register a Function
curl -X POST http://127.0.0.1:3030/register \
-H “Content-Type: application/json” \
-d ‘{
“name”: “add”,
“code”: “(module (func (export \”add\”) (param i32 i32) (result i32) local.get 0 local.get 1 i32.add))”
}’
Invoke the Function
curl -X POST http://127.0.0.1:3030/invoke \
-H “Content-Type: application/json” \
-d ‘{
“name”: “add”,
“input”: [3, 7]
}’
Adding Unit Tests
1. Unit Tests for storage.rs
The storage tests will verify that functions can be saved and loaded correctly.
Updated storage.rs
:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_save_and_load_function() {
let storage = Storage::init_with_path(“test_storage”).expect(“Failed to initialize storage”);
let name = “test_function”;
let code = “(module (func (export \”test\”) (param i32) (result i32) local.get 0))”;
storage
.save_function(name.to_string(), code.to_string())
.expect(“Failed to save function”);
let loaded_code = storage
.load_function(name)
.expect(“Failed to load function”);
assert_eq!(code, loaded_code);
std::fs::remove_dir_all(“test_storage”).expect(“Failed to clean up test storage”);
}
#[test]
fn test_load_nonexistent_function() {
let storage = Storage::init_with_path(“test_storage”).expect(“Failed to initialize storage”);
let result = storage.load_function(“nonexistent_function”);
assert!(result.is_err());
std::fs::remove_dir_all(“test_storage”).expect(“Failed to clean up test storage”);
}
}
2. Unit Tests for executor.rs
The executor tests will verify that WebAssembly functions can be executed with correct inputs and outputs.
Updated executor.rs
:
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_execute_valid_wasm() {
let wasm_code = r#”
(module
(func (export “add”) (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
)”#;
let result = execute(wasm_code, “add”, &[json!(3), json!(7)])
.expect(“Execution failed”);
assert_eq!(result, json!(10));
}
#[test]
fn test_execute_invalid_function() {
let wasm_code = r#”
(module
(func (export “add”) (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
)”#;
let result = execute(wasm_code, “subtract”, &[json!(3), json!(7)]);
assert!(result.is_err());
}
#[test]
fn test_execute_with_invalid_input() {
let wasm_code = r#”
(module
(func (export “add”) (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
)”#;
let result = execute(wasm_code, “add”, &[json!(3), json!(“invalid”)]);
assert!(result.is_err());
}
}
3. Integration Tests for api.rs
Integration tests ensure that the HTTP API works end-to-end. These tests simulate HTTP requests using Warp’s testing utilities.
Create tests/api_tests.rs
:
use warp::test::request;
use serverless_rust::api;
use serverless_rust::storage::Storage;
use std::sync::Arc;
#[tokio::test]
async fn test_register_function() {
let storage = Arc::new(Storage::init_with_path(“test_storage”).expect(“Failed to initialize storage”));
let body = r#”
{
“name”: “add”,
“code”: “(module (func (export \”add\”) (param i32 i32) (result i32) local.get 0 local.get 1 i32.add))”
}”#;
let response = request()
.method(“POST”)
.path(“/register”)
.body(body)
.reply(&api::server(storage.clone()))
.await;
assert_eq!(response.status(), 200);
std::fs::remove_dir_all(“test_storage”).expect(“Failed to clean up test storage”);
}
#[tokio::test]
async fn test_invoke_function() {
let storage = Arc::new(Storage::init_with_path(“test_storage”).expect(“Failed to initialize storage”));
storage
.save_function(
“add”.to_string(),
“(module (func (export \”add\”) (param i32 i32) (result i32) local.get 0 local.get 1 i32.add))”
.to_string(),
)
.expect(“Failed to save function”);
let body = r#”
{
“name”: “add”,
“input”: [3, 7]
}”#;
let response = request()
.method(“POST”)
.path(“/invoke”)
.body(body)
.reply(&api::server(storage.clone()))
.await;
assert_eq!(response.status(), 200);
assert!(std::str::from_utf8(response.body())
.expect(“Invalid UTF-8”)
.contains(“10”));
std::fs::remove_dir_all(“test_storage”).expect(“Failed to clean up test storage”);
}
4. Running All Tests
To run all tests in the project:
cargo test
If everything is implemented correctly, the tests should pass, confirming that:
- Functions can be saved, loaded, and retrieved from storage.
- WebAssembly modules can be executed with correct results.
- The HTTP API supports registering and invoking functions seamlessly.
This test suite ensures the serverless platform behaves as expected across all components.
Access the Complete Code
The entire codebase for this serverless platform is available on GitHub:
https://github.com/luishsr/rust-serverless
Feel free to clone the repository, explore the code, and extend the platform with new features!

🚀 Discover More Free Software Engineering Content! 🌟
If you enjoyed this post, be sure to explore my new software engineering blog, packed with 200+ in-depth articles, 🎥 explainer videos, 🎙️ a weekly software engineering podcast, 📚 books, 💻 hands-on tutorials with GitHub code, including:
🌟 Developing a Fully Functional API Gateway in Rust — Discover how to set up a robust and scalable gateway that stands as the frontline for your microservices.
🌟 Implementing a Network Traffic Analyzer — Ever wondered about the data packets zooming through your network? Unravel their mysteries with this deep dive into network analysis.
🌟Implementing a Blockchain in Rust — a step-by-step breakdown of implementing a basic blockchain in Rust, from the initial setup of the block structure, including unique identifiers and cryptographic hashes, to block creation, mining, and validation, laying the groundwork.
and much more!
✅ 200+ In-depth software engineering articles
🎥 Explainer Videos — Explore Videos
🎙️ A brand-new weekly Podcast on all things software engineering — Listen to the Podcast
📚 Access to my books — Check out the Books
💻 Hands-on Tutorials with GitHub code
📞 Book a Call
👉 Visit, explore, and subscribe for free to stay updated on all the latest: Home Page
LinkedIn Newsletter: Stay ahead in the fast-evolving tech landscape with regular updates and insights on Rust, Software Development, and emerging technologies by subscribing to my newsletter on LinkedIn. Subscribe Here
🔗 Connect with Me:
- LinkedIn: Join my professional network for more insightful discussions and updates. Connect on LinkedIn
- X: Follow me on Twitter for quick updates and thoughts on Rust programming. Follow on Twitter
Wanna talk? Leave a comment or drop me a message!
All the best,
Luis Soares
luis@luissoares.dev
Lead Software Engineer | Blockchain & ZKP Protocol Engineer | 🦀 Rust | Web3 | Solidity | Golang | Cryptography | Author