In this guide, we are going to see how to take a front-end Angular app, a backend API written in Rust and compiled to Web Assembly, and deploy everything to a Serverless environment on the NEW Azure Static Web Apps service.
While the UI is written with Angular. The interesting part about this app is that the generator, the core part of the backend API, is entirely written in Rust, and then compiled to Web Assembly (or WASM for short). The public API is exposed behind a Node.js façade through a serverless Azure Function.
Let's start...
What are we creating?
We are going to build a Cat names generator app. I love cats, I bet you do too. This app allows you to discover unique cat names for your loved pet.
β‘οΈ Try the app LIVE at https://catsify.app β‘οΈ
Our app structure is the following (showing only the important parts):
.
βββ api
β βββ dist
β β βββ func
β βββ func
β βββ ...
β βββ Cargo.toml
β βββ rust
βββ app
β βββ dist
β β βββ ui
β βββ ...
β βββ src
βββ ...
βββ scripts
Some highlights:
-
api
is a standard Azure Functions App folder. -
api/func
contains the Node.js serverless function. -
api/rust
contains the Rust source code. -
app
contains the Angular source code.
And...
-
api/dist/func
contains the API build artifacts. -
app/dist/ui
contains the APP build artifacts.
We will next describe the role of each stack: Rust/WASM, Node.js. Angular; and then explain how each part is built and deployed.
How are we creating it?
Azure Functions: The Node.js Code
Our public backend API is a Node.js Azure Function that acts as a Façade.
In the ./api/func/index.ts
file, we simply import and invoke "a" generate()
function (see next section), get and send back the result to the client.
Here is a simplified version of the code:
const { generate } = require("./wasm_loader");
const func = async function (context, req) {
const name = await generate();
const [adjective, noun] = name.split(" ");
context.res = {
body: {
adjective,
noun,
},
};
};
export default func;
However, in the ./api/func/wasm_loader.ts
file, this is where the magic happens, we actually load the WASM module compiled from Rust (see the Rust story), invokes the generate_name_str
function, passing in a seed argument, and decode the resulted string output.
Here is a simplified version of the code:
const fs = require('fs');
const path = require('path');
// the WASM file is copied to dis/func during the build
const wasmFile = path.join(__dirname, 'generator.wasm');
// a bunch of utilities to decode the WASM binary
function getInt32Memory(wasm) {...}
function getUint8Memory(wasm) {...}
function getStringFromWasm(wasm, ptr, len) {...}
// export a JavaScript function
export const generate = async function() {
// load the WASM module
const bytes = new Uint8Array(fs.readFileSync(wasmFile));
const result = await WebAssembly.instantiate(bytes);
const wasm = await Promise.resolve(result.instance.exports);
// setup args
const retptr = 8;
const seed = Date.now() % 1000 | 0;
// invoke the WASM code
const ret = wasm.generate_name_str(retptr, seed);
// decode result
const memi32 = getInt32Memory(wasm);
const v0 = getStringFromWasm(...);
wasm.__wbindgen_free(...);
// this is final the decoded name
return v0;
};
The core API: The Rust Code
This is NOT a deep dive guide into Rust code or WASM. You can learn about Rust and WASM on the official Rust website.
As I mentioned earlier, the main part of the backend API is the names generator which is written in Rust.
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn generate_name_str(seed: i32) -> String {
// the seed is coming from the JS side
let a = seed % (ADJECTIVES.len() as i32);
let b = seed % (NOUNS.len() as i32);
[ADJECTIVES[a as usize].to_string(), " ".to_string(), NOUNS[b as usize].to_string()].join("")
}
const ADJECTIVES: [&str; 1116] = [
"aback",
"abaft",
//...
];
const NOUNS: [&str; 1201] = [
"abbey",
"abbie",
//...
];
// used for debugging only
pub fn main() {
println!("{:?}", generate_name_str(1));
}
Without going into many details, we basically use two Rust vectors to store the nouns and adjectives of the cats, then we construct a string by choosing a random value from each vector. The generate_name_str
function is exported from Rust to JavaScript using the #[wasm_bindgen]
outer attribute. This will enable us to call this function from the JavaScript code, and pass in the seed argument.
The UI: The Angular Code
The Angular app was generated with Angular CLI version 10.0.0-next.4. A classic setup!
How are we building it?
Azure Function
Our Node.js Azure Function Node.js code is written in TypeScript, hence we are using tsc
to transpile into JavaScript and output the result in the ./dist/func
folder.
cd api
tsc # will create ./dist/func/
Rust to WASM
For the Rust code, in order to compile it and generate the WASM module, we use the [wasm-pack](https://github.com/rustwasm/wasm-pack)
:
cd api
wasm-pack build # will create ./pkg/
And the following configuration in api/Cargo.toml
:
[dependencies]
wasm-bindgen = "0.2.58"
[lib]
crate-type = ["cdylib", "rlib"]
path = "rust/lib.rs"
[[bin]]
name = "generator"
path = "rust/lib.rs"
[profile.release]
lto = true
panic = "abort"
# Tell `rustc` to optimize for small code size.
opt-level = "s"
Note:
wasm-pack
generates a Node.js wrapper that loads the WASM module. Since we wrote our own loader, we are only interested in the*.wasm
file. We will then need to copy the WASM file to the./api/dist/func
:
cp pkg/catsify_bg.wasm dist/func/generator.wasm
The Angular build
Building the Angular app is straightforward. The Angular CLI takes care of everything:
ng build --prod
This command will generate the app bundle under ./app/dist/ui
.
Project Build Recap
cd api
tsc # builds the Azure function.
wasm-pack build # builds the WASM module.
cp pkg/catsify_bg.wasm \
dist/func/generator.wasm
cd ../app
ng build --prod # builds the front-end app.
Now that we have created a front-end Angular app, and a backend serverless API, what's the easiest to static serverless app on Azure?
Introducing: Azure Static Web Apps! π
Static Web Apps is a new offering from Azure App Service. It's a new, simplified hosting option for modern web apps powered by serverless APIs.
Static Web Apps offers:
- Free web hosting for static content like HTML, CSS, JavaScript, and images.
- Integrated API support provided by Azure Functions.
- First-party GitHub integration where repository changes trigger builds and deployments.
- Globally distributed static content, putting content closer to your users.
- Free SSL certificates, which are automatically renewed.
- Custom domains to provide branded customizations to your app.
- Seamless security model with a reverse-proxy when calling APIs, which requires no CORS configuration.
- Authentication provider integrations with Azure Active Directory, Facebook, Google, GitHub, and Twitter.
- Customizable authorization role definition and assignments.
- Back-end routing rules enabling full control over the content and routes you serve.
- Generated staging versions powered by pull requests enabling preview versions of your site before publishing.
Let's deploy our app, in 3 steps!
Connect the GitHub account
Provide the build information
Watch GitHub build and Deploy the app on Azure Static Web Apps
Note: The YML workflow file created by Azure Static Web Apps has been updated with custom build steps.
Try it
- Visit the Static Web Apps quickstart to build and deploy your first static web app in minutes.
- See the Static Web Apps documentation for more info about Static Web Apps. Follow the guided learning paths in Microsoft Learn for creating and publishing an Angular, React, Svelte, or Vue JavaScript app and API or for creating and publishing an app with the Gatsby static site generator.
Resources
- Azure Static Web Apps documentation
- Azure Functions documentation
- Create your first function using Visual Studio Code
- Try Azure For Free
manekinekko / catsify
Catsify is a simple and creative app that helps you to find a unique name for your lovely cat
What is Catsify?
Catsify is a Cat names generator, hosted on Azure Static Web Apps. The tech stack consists of:
- An UI written in Angular v10 (preview).
- An API written in Rust, compiled to WASM and exposed through a Node.js serverless Function.
Top comments (0)